Skip to content

Instantly share code, notes, and snippets.

@halspg
Last active February 17, 2023 13:17
Show Gist options
  • Save halspg/a98fd45c6b0c52d57abe1b1b3b57062e to your computer and use it in GitHub Desktop.
Save halspg/a98fd45c6b0c52d57abe1b1b3b57062e to your computer and use it in GitHub Desktop.
An application that searches music with the iTunes API.
#! 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()
# 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