Last active
February 17, 2023 13:17
-
-
Save halspg/a98fd45c6b0c52d57abe1b1b3b57062e to your computer and use it in GitHub Desktop.
An application that searches music with the iTunes API.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! python3 | |
# coding: utf-8 | |
# Created for Pythonista 3.4 beta. | |
# An application that searches music with the iTunes API. | |
import console | |
import ui | |
import requests | |
import dialogs | |
import clipboard | |
import json | |
import os | |
import webbrowser | |
import photos | |
from pathlib import Path | |
import uuid | |
import time | |
import functools | |
# https://github.com/tdamdouni/Pythonista/blob/master/slider/SliderWithLabel_danrcook.py | |
from SliderWithLabel_danrcook import SliderWithLabel | |
# Settings | |
# Device screen size | |
# Can be checked with `ui.get_screen_size()` | |
w, h = (375, 812) | |
# Countries to search in iTunes Search API | |
country = "JP" | |
# Whether to open the results in iTunes (boolian) | |
open_in_itunes = True | |
# from https://github.com/tdamdouni/Pythonista/blob/master/ui/on-the-fly-grid-to-help-position-ui-elements.py | |
def grid_rc_(bounds, rows=1, columns=1): | |
# a grid based on rows and columns | |
# return a list of lists of ui.Rects | |
r = ui.Rect(*bounds) | |
w = r.width / columns | |
h = r.height / rows | |
rl = [] | |
for i in range(rows): | |
lst = [] | |
for j in range(columns): | |
lst.append(ui.Rect(j * w, h * i, w, h)) | |
rl.append(lst) | |
return rl | |
def itunes_search(action, param): | |
j = requests.get(f"http://itunes.apple.com/{country}/{action}", params=param) | |
return j.json()["results"] | |
class search_view(object): | |
def __init__(self): | |
self.limit = 20 | |
self.max_val = 200 | |
self.min_val = 1 | |
self.view = ui.View(frame=(0, 0, w, h)) | |
self.view.name = "iTunes Music Search" | |
self.view.background_color = "white" | |
comp_h = 40 | |
radius = 20 | |
vb = self.view.bounds | |
self.text = ui.TextField() | |
self.text.frame = grid_rc_(vb, 16, 10)[0][1] | |
self.text.name = "term" | |
self.text.width = w * 0.8 | |
self.text.height = comp_h | |
self.text.border_width = 1 | |
self.text.corner_radius = radius | |
self.text.border_color = "#cdcdcd" | |
self.text.flex = "lr" | |
self.text.clear_button_mode = "while_editing" | |
self.text.placeholder = "Enter search term" | |
self.sg = ui.SegmentedControl() | |
self.sg.background_color = "#f4f4f4" | |
self.sg.frame = grid_rc_(vb, 14, 4)[1][1] | |
self.sg.width = w * 0.5 | |
self.sg.height = comp_h | |
self.sg.flex = "lr" | |
self.sg.segments = ["Song", "Album"] | |
self.sg.selected_index = 0 | |
self.sl = SliderWithLabel( | |
name="slider", | |
frame=grid_rc_(vb, 14, 10)[2][1], | |
value=20, | |
max_val=200, | |
min_val=1, | |
label_text_color="#5a5a5a", | |
label_background_color="#f4f4f4", | |
) | |
self.sl.width = w * 0.8 | |
self.sl.flex = "lr" | |
self.button = ui.Button(title="Search") | |
self.button.frame = grid_rc_(vb, 14, 6)[4][2] | |
self.button.width = w * 0.33 | |
self.button.height = comp_h | |
self.button.flex = "lr" | |
self.button.border_width = 1 | |
self.button.border_color = "#cdcdcd" | |
self.button.corner_radius = radius | |
self.button.background_color = "#f4f4f4" | |
self.button.tint_color = "#5a5a5a" | |
self.button.action = self.button_tapped | |
self.view.add_subview(self.button) | |
self.view.add_subview(self.text) | |
self.view.add_subview(self.sg) | |
self.view.add_subview(self.sl) | |
# self.view.add_subview(self.tf) | |
def button_tapped(self, sender): | |
if self.text.text: | |
param = { | |
"term": self.text.text, | |
"country": country, | |
"media": "music", | |
"entity": self.sg.segments[self.sg.selected_index].lower(), | |
"limit": int(self.sl.label.text), | |
} | |
s = itunes_search("search", param) | |
self.view.navigation_view.push_view( | |
result_view(self.text.text, s).tableview | |
) | |
else: | |
pass | |
def set_slider_value(self, sender): | |
self.tf.text = str( | |
round(self.sl.value * (self.max_val - self.min_val) + self.min_val) | |
) | |
class result_view(object): | |
def __init__(self, term, json): | |
self.term = term | |
self.j = json | |
self.tableview = ui.TableView() | |
self.tableview.frame = (0, 0, *(ui.get_screen_size())) | |
self.tableview.name = "Search Result" | |
self.tableview.background_color = "white" | |
self.tableview.separator_color = (1, 1, 1, 0) | |
self.tableview.data_source = self | |
self.tableview.delegate = self | |
def tableview_number_of_sections(self, tableview): | |
return 1 | |
def tableview_title_for_header(self, tableview, section): | |
return f'Search term: "{self.term}"' | |
def tableview_number_of_rows(self, tableview, section): | |
return len(self.j) | |
def tableview_cell_for_row(self, tableview, section, row): | |
cell = ui.TableViewCell("subtitle") | |
cell.background_color = "white" | |
cell.frame = tableview.bounds | |
if self.j[row]["wrapperType"] == "collection": | |
cell.text_label.text = self.j[row]["collectionCensoredName"] | |
cell.detail_text_label.text = self.j[row]["artistName"] | |
cell.image_view.image = ui.Image.named("iob:disc_32") | |
elif self.j[row]["wrapperType"] == "track": | |
cell.text_label.text = self.j[row]["trackCensoredName"] | |
cell.detail_text_label.text = ( | |
f"{self.j[row]['artistName']} / {self.j[row]['collectionCensoredName']}" | |
) | |
cell.image_view.image = ui.Image.named("iob:ios7_musical_note_32") | |
cell.accessory_type = "detail_disclosure_button" | |
if row % 2: | |
cell.background_color = "#f0f0f0" | |
return cell | |
def tableview_accessory_button_tapped(self, tableview, section, row): | |
image_view(self.j[row]["artworkUrl100"]) | |
def tableview_did_select(self, tableview, section, row): | |
if self.j[row]["wrapperType"] == "track": | |
tableview.navigation_view.push_view(json_view(self.j[row]).tableview) | |
else: | |
tableview.navigation_view.push_view(tracklist_view(self.j[row]).tableview) | |
class json_view(object): | |
def __init__(self, json): | |
self.tableview = ui.TableView() | |
self.tableview.frame = (0, 0, w, h) | |
self.tableview.background_color = "white" | |
self.tableview.separator_color = (1, 1, 1, 0) | |
self.json = json | |
try: | |
self.selected_title = json["trackCensoredName"] | |
except KeyError: | |
self.selected_title = json["collectionCensoredName"] | |
self.dict_keys = list(self.json.keys()) | |
self.dict_values = list(self.json.values()) | |
self.tableview.data_source = self | |
self.tableview.delegate = self | |
self.share_button = ui.ButtonItem() | |
self.share_button.image = ui.Image.named("iob:share_32") | |
self.share_button.action = self.share_json | |
self.itunes_button = ui.ButtonItem() | |
self.itunes_button.image = ui.Image.named("iob:ios7_world_32") | |
self.itunes_button.action = self.open_app | |
self.youtube_button = ui.ButtonItem() | |
self.youtube_button.image = ui.Image.named("iob:social_youtube_32") | |
self.youtube_button.action = self.search_youtube | |
self.tableview.right_button_items = [ | |
self.youtube_button, | |
self.itunes_button, | |
self.share_button, | |
] | |
def tableview_number_of_sections(self, tableview): | |
return 1 | |
def tableview_title_for_header(self, tableview, section): | |
return self.selected_title | |
def tableview_number_of_rows(self, tableview, section): | |
return len(self.dict_keys) | |
def tableview_cell_for_row(self, tableview, section, row): | |
cell = ui.TableViewCell("subtitle") | |
cell.background_color = "white" | |
try: | |
cell.text_label.text = self.dict_values[row] | |
except TypeError: | |
cell.text_label.text = str(self.dict_values[row]) | |
cell.detail_text_label.text = self.dict_keys[row] | |
if row % 2: | |
cell.background_color = "#f0f0f0" | |
return cell | |
def tableview_accessory_button_tapped(self, tableview, section, row): | |
pass | |
def tableview_did_select(self, tableview, section, row): | |
out = self.dict_values[row] | |
clipboard.set(out if isinstance(out, str) else str(out)) | |
console.hud_alert("Copy", "success", 0.5) | |
def share_json(self, *args): | |
dialogs.share_text(json.dumps(self.json, indent=4, ensure_ascii=False)) | |
def open_app(self, *args): | |
open_itunes = '?app=itunes' if open_in_itunes is True else '' | |
url = f"safari-https://itunes.apple.com/jp/album/id{self.json['collectionId']}{open_itunes}" | |
webbrowser.open(url) | |
def search_youtube(self, *args): | |
import urllib.parse | |
try: | |
term = f"{self.json['artistName']} {self.json['trackCensoredName']}" | |
except KeyError: | |
f"{self.json['artistName']} {self.json['collectionCensoredName']}" | |
url = f"safari-https://www.youtube.com/results?search_query={urllib.parse.quote(term)}" | |
webbrowser.open(url) | |
class tracklist_view(object): | |
def __init__(self, json): | |
self.src = json | |
self.j = self.get_tracks() | |
self.tableview = ui.TableView() | |
self.tableview.frame = (0, 0, w, h) | |
self.tableview.name = "Track List" | |
self.tableview.background_color = "white" | |
self.tableview.separator_color = (1, 1, 1, 0) | |
self.tableview.data_source = self | |
self.tableview.delegate = self | |
def tableview_number_of_sections(self, tableview): | |
return 1 | |
def tableview_title_for_header(self, tableview, section): | |
return f'Album Title: "{self.src["collectionCensoredName"]}"' | |
def tableview_number_of_rows(self, tableview, section): | |
return len(self.j) | |
def tableview_cell_for_row(self, tableview, section, row): | |
cell = ui.TableViewCell("subtitle") | |
cell.background_color = "white" | |
cell.frame = tableview.bounds | |
if self.j[row]["wrapperType"] == "collection": | |
cell.text_label.text = self.j[row]["collectionCensoredName"] | |
cell.detail_text_label.text = self.j[row]["artistName"] | |
cell.image_view.image = ui.Image.named("iob:disc_32") | |
cell.accessory_type = "detail_disclosure_button" | |
elif self.j[row]["wrapperType"] == "track": | |
cell.text_label.text = f"{str(self.j[row]['trackNumber']).zfill(2)}. {self.j[row]['trackCensoredName']}" | |
cell.detail_text_label.text = f"{self.j[row]['artistName']}" | |
cell.image_view.image = ui.Image.named("iob:ios7_musical_note_32") | |
cell.accessory_type = "disclosure_indicator" | |
if row % 2: | |
cell.background_color = "#f0f0f0" | |
return cell | |
def tableview_accessory_button_tapped(self, tableview, section, row): | |
image_view(self.j[row]["artworkUrl100"]) | |
def tableview_did_select(self, tableview, section, row): | |
tableview.navigation_view.push_view(json_view(self.j[row]).tableview) | |
def get_tracks(self): | |
param = {"id": self.src["collectionId"], "country": country, "entity": "song"} | |
s = itunes_search("lookup", param) | |
return s | |
class image_view(object): | |
def __init__(self, artwork_url): | |
self.v = ui.View(frame=(0, 0, w, w)) | |
self.v.name = 'image_view' | |
self.v.tint_color = "#818181" | |
self.v.name = "Artwork" | |
self.iv = ui.ImageView(frame=(0, 0, w, w)) | |
self.iv.flex = "lr" | |
self.iv.background_color = "#f0f0f0" | |
self.aw_url = artwork_url.replace(os.path.basename(artwork_url), "600x600bb.jpg") | |
self.aw_url_large = artwork_url.replace("100x100bb", "100000x100000-999") | |
self.ext = Path(artwork_url).suffix | |
self.ai = ui.ActivityIndicator() | |
self.ai.style = ui.ACTIVITY_INDICATOR_STYLE_WHITE_LARGE | |
self.ai.flex = 'lr' | |
length = w / 3 | |
with ui.ImageContext(length, length) as ctx: | |
rect = ui.Path.rounded_rect(0, 0, length, length, 20) | |
ui.set_color('#000000') | |
ui.set_alpha(0.4) | |
rect.fill() | |
ic = ctx.get_image() | |
self.ic = ui.ImageView() | |
self.ic.frame = (0, 0, length, length) | |
self.ic.flex = 'lr' | |
self.ic.image = ic | |
self.label = ui.Label() | |
self.label.font = ('<system-bold>', 14) | |
self.label.text_color = 'white' | |
self.label.alignment = ui.ALIGN_CENTER | |
self.label.text = 'Saved' | |
self.label.frame = (0, 0, length/3, length/3) | |
self.label.flex = 'lr' | |
self.save_button = ui.ButtonItem() | |
self.save_button.image = ui.Image.named("iob:ios7_download_outline_24") | |
self.save_button.action = functools.partial(self.save_artwork, img_size="normal") | |
self.save_l_button = ui.ButtonItem() | |
self.save_l_button.image = ui.Image.named("iob:ios7_download_outline_32") | |
self.save_l_button.action = functools.partial(self.save_artwork, img_size="large") | |
self.v.right_button_items = [self.save_l_button] | |
self.v.left_button_items = [self.save_button] | |
self.v.add_subview(self.iv) | |
self.v.add_subview(self.ai) | |
self.v.present("sheet") | |
def load_image(): | |
self.iv.load_from_url(self.aw_url) | |
ui.delay(load_image, 0.5) | |
def save_artwork_preprocessing(self): | |
width , height = ui.get_screen_size() | |
center = (width/2, height/2) | |
self.ai.center = center | |
self.ic.center = center | |
self.label.center = center | |
self.save_button.enabled = False | |
self.save_l_button.enabled = False | |
self.v.add_subview(self.ic) | |
self.ai.start() | |
self.ai.bring_to_front() | |
def save_artwork_postprocessing(self): | |
self.ai.stop() | |
self.v.add_subview(self.label) | |
time.sleep(0.5) | |
self.v.remove_subview(self.label) | |
self.v.remove_subview(self.ic) | |
self.save_button.enabled = True | |
self.save_l_button.enabled = True | |
def save_image_data(self, img): | |
tempfilename = f"{uuid.uuid4()}{self.ext}" | |
with open(tempfilename, 'wb') as f: | |
f.write(img) | |
photos.create_image_asset(tempfilename) | |
os.remove(tempfilename) | |
@ui.in_background | |
def save_artwork(self, *args, img_size="normal"): | |
self.save_artwork_preprocessing() | |
if img_size == "normal": | |
while True: | |
if isinstance(self.iv.image, ui.Image): | |
break | |
self.save_image_data(self.iv.image.to_jpeg()) | |
else: | |
r = requests.get(self.aw_url_large).content | |
self.save_image_data(r) | |
self.save_artwork_postprocessing() | |
if __name__ == "__main__": | |
sv = search_view() | |
nv = ui.NavigationView(sv.view) | |
nv.tint_color = "#818181" | |
nv.background_color = "white" | |
nv.present("sheet") | |
sv.text.begin_editing() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://gist.github.com/danrcook/5b35e47628d28daec1d5ec7e909b4f95 | |
# original: https://github.com/tdamdouni/Pythonista/blob/master/slider/SliderWithLabel_danrcook.py | |
''' | |
SliderWithLabel is a wrapper for ui.Slider() for use in Pythonista on iOS. Provides an editable label for the display and setting of the slider value. | |
See SliderWithLabel class for detailed usage. | |
''' | |
__author__ = '@cook' | |
import ui | |
class SliderWithLabel(ui.View): | |
'''Wrapper for ui.Slider to also show a label. You can edit the value of the slider directly in the label since it is a textfield. | |
Keyword Arguments for SliderWithLabel: | |
-action: define a function to receive information when a slider has been slid | |
SliderWithLabel can be initialized with arguments specific to the slider and label: | |
Keyword Arguments for the slider: | |
- value: default value when presented (should less than max_val and greater than 0). Default is 50 | |
- max_val: the default for a usual slider is 1.0. SliderWithLabel will conventiently multiply the max_val for the label display and for returning it's value attribute. The default is 100 | |
- slider_tint_color for the color of the slider bar (up to current point). Default is 0.7 (gray) | |
Keyword arguments for the label: | |
- label_bgcolor: default is 1 (white) | |
- label_border_color: default is 0.7 (light gray) | |
- label_border_width: default is 0.5 | |
- label_corner_radius: default is 5 | |
- label_font: default is ('<system>', 11) | |
- label_text_color: default is 0.7 | |
- label_keyboard_type: default is ui.KEYBOARD_DECIMAL_PAD | |
- label_alignment: default is ui.ALIGN_CENTER | |
- label_alpha: default is 1 | |
- label_bordered: default is False (this will override style attributes and just be a white rect with frame) | |
Other: | |
- values are rounded in the label and for SliderWithLabel.value | |
- SliderWithLabel needs some vertical space: recommonded height of 60 | |
- use SliderWithLabel.value for returning a value between 0 and SliderWithLabel.max_val | |
''' | |
def __init__(self, **kwargs): | |
super().__init__(**kwargs) | |
self.max_val = kwargs['max_val'] if 'max_val' in kwargs else 100 | |
# add min_val | |
self.min_val = kwargs['min_val'] if 'min_val' in kwargs else 1 | |
self.value = round(kwargs['value']) if 'value' in kwargs else round(self.max_val/2) #for convenience in getting the value attribute | |
self.make_view(**kwargs) | |
self.action = kwargs['action'] if 'action' in kwargs else None | |
def make_view(self, **kwargs): | |
slider = ui.Slider() | |
slider.value = kwargs['value']/self.max_val if 'value' in kwargs else 0.5 | |
slider.action = self.update_label_and_value | |
slider.tint_color = kwargs.get('slider_tint_color', 0.7) | |
label = self.make_label(**kwargs) | |
self.add_subview(slider) | |
self.add_subview(label) | |
self.label = label | |
self.slider = slider | |
def make_label(self, **kwargs): | |
label = ui.TextField() | |
# change touch_enabled | |
label.touch_enabled = False | |
label.action = self.update_value | |
label.text = str(self.value) | |
label.bordered = False | |
defaults = {'background_color': 1, | |
'border_color': 0.7, | |
'border_width': 0.5, | |
'corner_radius': 5, | |
'font': ('<system>', 11), | |
'text_color': 0.7, | |
'keyboard_type': ui.KEYBOARD_DECIMAL_PAD, | |
'alignment': ui.ALIGN_CENTER, | |
'alpha': 1, | |
'bordered': False, | |
'frame': (0,0,100,100)} | |
for attr in defaults: | |
setattr(label, attr, kwargs['label_' + attr]) if 'label_' + attr in kwargs\ | |
else setattr(label, attr, defaults[attr]) | |
if label.bordered: | |
label.background_color = (0,0,0,0) #because the background will still show on bordered | |
return label | |
def update_value(self, sender): | |
try: #try/except in case wrong text is entered... | |
if 0 < int(self.label.text) < self.max_val: | |
self.slider.value = int(self.label.text)/self.max_val | |
self.update_label_and_value(self) | |
else: | |
self.label.text = str(self.value) #if out of slider min/max reset label to previous | |
except: | |
self.label.text = str(self.value) #essentially do nothing for wrong text, reset to previous | |
def update_label_and_value(self, sender): | |
self.label.x = (self.slider.width - 34) * self.slider.value - (self.label.width/2) + 17 | |
# change value | |
# self.value = round(self.slider.value*self.max_val) | |
self.value = round(self.slider.value*(self.max_val-self.min_val)+self.min_val) | |
self.label.text = str(self.value) | |
#so the label doesn't go off-view: | |
if self.label.x + self.label.width > self.width: | |
self.label.x = self.width - self.label.width | |
if self.label.x < 0: | |
self.label.x = 0 | |
#for sending self to action | |
if self.action and callable(self.action): | |
self.action(self) | |
def layout(self): | |
#some of these constants are just to define the layout for the sake of design | |
self.height = 60 if self.height < 60 else self.height #should be 60 or more otherwise clipping will occur | |
self.slider.frame = (0,self.height/2-7,self.width, 34) | |
self.label.width, self.label.height = 46, 20 | |
self.label.y = self.slider.y - (self.label.height + 2) | |
self.label.x = (self.slider.width - 34) * self.slider.value - (self.label.width/2) + 17 | |
if __name__ == '__main__': | |
#simple example of three sliders with different names | |
#ability to set a function as an action for the SliderWithLabel | |
view = ui.View() | |
def get_value(sender): | |
view.name = str(sender.value) | |
w = ui.get_window_size()[0] | |
# a = SliderWithLabel(name='a', frame=(10,30,w-20,60), value=20, max_val=200, label_background_color='#223322', action=get_value) | |
a = SliderWithLabel(name='a', frame=(10,30,w-20,60), value=20, max_val=200, min_val=1, label_background_color='#223322', action=get_value) | |
b = SliderWithLabel(name='b', frame=(10,90,w-20,60), value=75, max_val=100, action=get_value) | |
c = SliderWithLabel(name='c', frame=(10,150,w-20,60), max_val=1000, label_background_color='#aa0000', label_text_color=1, action=get_value) | |
view.add_subview(a) | |
view.add_subview(b) | |
view.add_subview(c) | |
view.background_color = 1 | |
view.present() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment