utils.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import datetime
  2. import math
  3. import os
  4. import platform
  5. import re
  6. import subprocess
  7. from enum import Enum
  8. from typing import List, Tuple
  9. import music_tag
  10. import requests
  11. from const import ARTIST, GENRE, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
  12. WINDOWS_SYSTEM, ALBUMARTIST
  13. from zspotify import ZSpotify
  14. class MusicFormat(str, Enum):
  15. MP3 = 'mp3',
  16. OGG = 'ogg',
  17. def create_download_directory(download_path: str) -> None:
  18. """ Create directory and add a hidden file with song ids """
  19. os.makedirs(download_path, exist_ok=True)
  20. # add hidden file with song ids
  21. hidden_file_path = os.path.join(download_path, '.song_ids')
  22. if not os.path.isfile(hidden_file_path):
  23. with open(hidden_file_path, 'w', encoding='utf-8') as f:
  24. pass
  25. def get_previously_downloaded() -> List[str]:
  26. """ Returns list of all time downloaded songs """
  27. ids = []
  28. archive_path = ZSpotify.CONFIG.get_song_archive()
  29. if os.path.exists(archive_path):
  30. with open(archive_path, 'r', encoding='utf-8') as f:
  31. ids = [line.strip().split('\t')[0] for line in f.readlines()]
  32. return ids
  33. def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str) -> None:
  34. """ Adds song id to all time installed songs archive """
  35. archive_path = ZSpotify.CONFIG.get_song_archive()
  36. if os.path.exists(archive_path):
  37. with open(archive_path, 'a', encoding='utf-8') as file:
  38. file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
  39. else:
  40. with open(archive_path, 'w', encoding='utf-8') as file:
  41. file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
  42. def get_directory_song_ids(download_path: str) -> List[str]:
  43. """ Gets song ids of songs in directory """
  44. song_ids = []
  45. hidden_file_path = os.path.join(download_path, '.song_ids')
  46. if os.path.isfile(hidden_file_path):
  47. with open(hidden_file_path, 'r', encoding='utf-8') as file:
  48. song_ids.extend([line.strip().split('\t')[0] for line in file.readlines()])
  49. return song_ids
  50. def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, author_name: str, song_name: str) -> None:
  51. """ Appends song_id to .song_ids file in directory """
  52. hidden_file_path = os.path.join(download_path, '.song_ids')
  53. # not checking if file exists because we need an exception
  54. # to be raised if something is wrong
  55. with open(hidden_file_path, 'a', encoding='utf-8') as file:
  56. file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
  57. def get_downloaded_song_duration(filename: str) -> float:
  58. """ Returns the downloaded file's duration in seconds """
  59. command = ['ffprobe', '-show_entries', 'format=duration', '-i', f'{filename}']
  60. output = subprocess.run(command, capture_output=True)
  61. duration = re.search(r'[\D]=([\d\.]*)', str(output.stdout)).groups()[0]
  62. duration = float(duration)
  63. return duration
  64. def split_input(selection) -> List[str]:
  65. """ Returns a list of inputted strings """
  66. inputs = []
  67. if '-' in selection:
  68. for number in range(int(selection.split('-')[0]), int(selection.split('-')[1]) + 1):
  69. inputs.append(number)
  70. else:
  71. selections = selection.split(',')
  72. for i in selections:
  73. inputs.append(i.strip())
  74. return inputs
  75. def splash() -> str:
  76. """ Displays splash screen """
  77. return """
  78. ███████ ███████ ██████ ██████ ████████ ██ ███████ ██ ██
  79. ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
  80. ███ ███████ ██████ ██ ██ ██ ██ █████ ████
  81. ███ ██ ██ ██ ██ ██ ██ ██ ██
  82. ███████ ███████ ██ ██████ ██ ██ ██ ██
  83. """
  84. def clear() -> None:
  85. """ Clear the console window """
  86. if platform.system() == WINDOWS_SYSTEM:
  87. os.system('cls')
  88. else:
  89. os.system('clear')
  90. def set_audio_tags(filename, artists, genres, name, album_name, release_year, disc_number, track_number) -> None:
  91. """ sets music_tag metadata """
  92. tags = music_tag.load_file(filename)
  93. tags[ALBUMARTIST] = artists[0]
  94. tags[ARTIST] = conv_artist_format(artists)
  95. tags[GENRE] = genres[0] if not ZSpotify.CONFIG.get_all_genres() else ZSpotify.CONFIG.get_all_genres_delimiter().join(genres)
  96. tags[TRACKTITLE] = name
  97. tags[ALBUM] = album_name
  98. tags[YEAR] = release_year
  99. tags[DISCNUMBER] = disc_number
  100. tags[TRACKNUMBER] = track_number
  101. tags.save()
  102. def conv_artist_format(artists) -> str:
  103. """ Returns converted artist format """
  104. return ', '.join(artists)
  105. def set_music_thumbnail(filename, image_url) -> None:
  106. """ Downloads cover artwork """
  107. img = requests.get(image_url).content
  108. tags = music_tag.load_file(filename)
  109. tags[ARTWORK] = img
  110. tags.save()
  111. def regex_input_for_urls(search_input) -> Tuple[str, str, str, str, str, str]:
  112. """ Since many kinds of search may be passed at the command line, process them all here. """
  113. track_uri_search = re.search(
  114. r'^spotify:track:(?P<TrackID>[0-9a-zA-Z]{22})$', search_input)
  115. track_url_search = re.search(
  116. r'^(https?://)?open\.spotify\.com/track/(?P<TrackID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  117. search_input,
  118. )
  119. album_uri_search = re.search(
  120. r'^spotify:album:(?P<AlbumID>[0-9a-zA-Z]{22})$', search_input)
  121. album_url_search = re.search(
  122. r'^(https?://)?open\.spotify\.com/album/(?P<AlbumID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  123. search_input,
  124. )
  125. playlist_uri_search = re.search(
  126. r'^spotify:playlist:(?P<PlaylistID>[0-9a-zA-Z]{22})$', search_input)
  127. playlist_url_search = re.search(
  128. r'^(https?://)?open\.spotify\.com/playlist/(?P<PlaylistID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  129. search_input,
  130. )
  131. episode_uri_search = re.search(
  132. r'^spotify:episode:(?P<EpisodeID>[0-9a-zA-Z]{22})$', search_input)
  133. episode_url_search = re.search(
  134. r'^(https?://)?open\.spotify\.com/episode/(?P<EpisodeID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  135. search_input,
  136. )
  137. show_uri_search = re.search(
  138. r'^spotify:show:(?P<ShowID>[0-9a-zA-Z]{22})$', search_input)
  139. show_url_search = re.search(
  140. r'^(https?://)?open\.spotify\.com/show/(?P<ShowID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  141. search_input,
  142. )
  143. artist_uri_search = re.search(
  144. r'^spotify:artist:(?P<ArtistID>[0-9a-zA-Z]{22})$', search_input)
  145. artist_url_search = re.search(
  146. r'^(https?://)?open\.spotify\.com/artist/(?P<ArtistID>[0-9a-zA-Z]{22})(\?si=.+?)?$',
  147. search_input,
  148. )
  149. if track_uri_search is not None or track_url_search is not None:
  150. track_id_str = (track_uri_search
  151. if track_uri_search is not None else
  152. track_url_search).group('TrackID')
  153. else:
  154. track_id_str = None
  155. if album_uri_search is not None or album_url_search is not None:
  156. album_id_str = (album_uri_search
  157. if album_uri_search is not None else
  158. album_url_search).group('AlbumID')
  159. else:
  160. album_id_str = None
  161. if playlist_uri_search is not None or playlist_url_search is not None:
  162. playlist_id_str = (playlist_uri_search
  163. if playlist_uri_search is not None else
  164. playlist_url_search).group('PlaylistID')
  165. else:
  166. playlist_id_str = None
  167. if episode_uri_search is not None or episode_url_search is not None:
  168. episode_id_str = (episode_uri_search
  169. if episode_uri_search is not None else
  170. episode_url_search).group('EpisodeID')
  171. else:
  172. episode_id_str = None
  173. if show_uri_search is not None or show_url_search is not None:
  174. show_id_str = (show_uri_search
  175. if show_uri_search is not None else
  176. show_url_search).group('ShowID')
  177. else:
  178. show_id_str = None
  179. if artist_uri_search is not None or artist_url_search is not None:
  180. artist_id_str = (artist_uri_search
  181. if artist_uri_search is not None else
  182. artist_url_search).group('ArtistID')
  183. else:
  184. artist_id_str = None
  185. return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str
  186. def fix_filename(name):
  187. """
  188. Replace invalid characters on Linux/Windows/MacOS with underscores.
  189. List from https://stackoverflow.com/a/31976060/819417
  190. Trailing spaces & periods are ignored on Windows.
  191. >>> fix_filename(" COM1 ")
  192. '_ COM1 _'
  193. >>> fix_filename("COM10")
  194. 'COM10'
  195. >>> fix_filename("COM1,")
  196. 'COM1,'
  197. >>> fix_filename("COM1.txt")
  198. '_.txt'
  199. >>> all('_' == fix_filename(chr(i)) for i in list(range(32)))
  200. True
  201. """
  202. return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", str(name), flags=re.IGNORECASE)
  203. def fmt_seconds(secs: float) -> str:
  204. val = math.floor(secs)
  205. s = math.floor(val % 60)
  206. val -= s
  207. val /= 60
  208. m = math.floor(val % 60)
  209. val -= m
  210. val /= 60
  211. h = math.floor(val)
  212. if h == 0 and m == 0 and s == 0:
  213. return "0s"
  214. elif h == 0 and m == 0:
  215. return f'{s}s'.zfill(2)
  216. elif h == 0:
  217. return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
  218. else:
  219. return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)