diff --git a/README.md b/README.md index 77c5eba..647f6c7 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@

+ @@ -76,6 +78,7 @@ MAC:

Command Line Usage

+``` Basic command line usage: python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Can take multiple urls. @@ -88,7 +91,6 @@ Different usage modes: Extra command line options: -ns, --no-splash Suppress the splash screen when loading. -<<<<<<< HEAD Options that can be configured in zs_config.json: ROOT_PATH Change this path if you don't like the default directory where ZSpotify saves the music @@ -105,17 +107,19 @@ Options that can be configured in zs_config.json: ======= --config-location Use a different zs_config.json, defaults to the one in the program directory -``` + ``` + + ### Options: All these options can either be configured in the zs_config or via the commandline, in case of both the commandline-option has higher priority. Be aware you have to set boolean values in the commandline like this: `--download-real-time=True` - +``` | Key (zs-config) | commandline parameter | Description |------------------------------|----------------------------------|---------------------------------------------------------------------| -| ROOT_PATH | --root-path | directory where ZSpotify saves the music -| ROOT_PODCAST_PATH | --root-podcast-path | directory where ZSpotify saves the podcasts +| ROOT_PATH | --root-path | Directory where ZSpotify saves the music +| ROOT_PODCAST_PATH | --root-podcast-path | Directory where ZSpotify saves the podcasts | SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name | SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Create a .song_archive file and skip previously downloaded songs | DOWNLOAD_FORMAT | --download-format | The download audio format (aac, fdk_aac, m4a, mp3, ogg, opus, vorbis) @@ -123,8 +127,8 @@ Be aware you have to set boolean values in the commandline like this: `--downloa | ANTI_BAN_WAIT_TIME | --anti-ban-wait-time | The wait time between bulk downloads | OVERRIDE_AUTO_WAIT | --override-auto-wait | Totally disable wait time between songs with the risk of instability | CHUNK_SIZE | --chunk-size | chunk size for downloading -| SPLIT_ALBUM_DISCS | --split-album-discs | split downloaded albums by disc -| DOWNLOAD_REAL_TIME | --download-real-time | only downloads songs as fast as they would be played, can prevent account bans +| SPLIT_ALBUM_DISCS | --split-album-discs | Split downloaded albums by disc +| DOWNLOAD_REAL_TIME | --download-real-time | Only downloads songs as fast as they would be played, can prevent account bans | LANGUAGE | --language | Language for spotify metadata | BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding | SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED @@ -136,12 +140,14 @@ Be aware you have to set boolean values in the commandline like this: `--downloa | PRINT_ERRORS | --print-errors | Print errors | PRINT_DOWNLOADS | --print-downloads | Print messages when a song is finished downloading | TEMP_DOWNLOAD_DIR | --temp-download-dir | Download tracks to a temporary directory first - +| ENABLE_MEDIA_KEYS | --enable-media-keys | Allows use of the media keys on your keyboard to play/pause, change track +| RELATIVE_TIME | --relative-time | Changes the song duration time to time remaining +``` ### Output format: With the option `OUTPUT` (or the commandline parameter `--output`) you can specify the output location and format. The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can contain the following placeholder: - +``` | Placeholder | Description |-----------------|-------------------------------- | {artist} | The song artist @@ -157,7 +163,7 @@ The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can c | {album_num} | (only when downloading albums) Incrementing track number | {playlist} | (only when downloading playlists) Name of the playlist | {playlist_num} | (only when downloading playlists) Incrementing track number - +``` Example values could be: ~~~~ {playlist}/{artist} - {song_name}.{ext} @@ -169,10 +175,9 @@ Liked Songs/{artist} - {song_name}.{ext} ~~~~ ### Docker Usage ->>>>>>> 1585133e70ad6ab21c70e07f5c9d98b1127eca3e +`>>>>>>> 1585133e70ad6ab21c70e07f5c9d98b1127eca3e` -

FAQ

-``` +## FAQ ### Will my account get banned if I use this tool? diff --git a/requirements.txt b/requirements.txt index 75f5aa7..577756b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ pyqtdarktheme python-vlc requests tqdm -pydub \ No newline at end of file +pydub +keyboard \ No newline at end of file diff --git a/source/app.py b/source/app.py index 5376345..af09ba1 100755 --- a/source/app.py +++ b/source/app.py @@ -3,7 +3,7 @@ from getpass import getpass import os from album import download_album, download_artist_albums -from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ +from const import TRACK, NAME, ID, ALBUM_ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist from podcast import download_episode, get_show_episodes @@ -182,6 +182,7 @@ def search(search_term): dics.append({ ID: track[ID], NAME: track[NAME], + ALBUM_ID: track[ALBUM][ID], 'type': TRACK, }) diff --git a/source/appGui.py b/source/appGui.py index 1b4a47b..8447c0d 100755 --- a/source/appGui.py +++ b/source/appGui.py @@ -9,6 +9,7 @@ import sys import requests import logging +from mutagen.id3 import ID3 from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import QThreadPool from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog, QTreeWidgetItem, QLineEdit @@ -76,6 +77,7 @@ def __init__(self, parent=None): self.download_tree.focus() self.reconnecting = False + def show(self): super().show() if not ZSpotify.login(): @@ -144,8 +146,11 @@ def display_results(self, results): def update_item_info(self, item, headers, labels): if not item: return self.selected_item = item - worker = Worker(self._cover_art_loader, item) - QThreadPool.globalInstance().start(worker) + if item.downloaded: + self.extract_cover_art(item) + else: + worker = Worker(self._cover_art_loader, item) + QThreadPool.globalInstance().start(worker) [lbl.setText("") for lbl in self.info_labels] if "Index" in headers: labels.pop(headers.index("Index")) @@ -183,6 +188,21 @@ def request_cover_art(self, url): lbl.setScaledContents(True) lbl.show() + def extract_cover_art(self, item): + if item.path == "": return + tags = ID3(item.path) + pict = [v for v in tags.values() if v.FrameID == "APIC"] + if not len(pict): + pass + if not pict[0]: + pass + pixmap = QPixmap() + pixmap.loadFromData(pict[0].data) + lbl = self.coverArtLabel + lbl.setPixmap(pixmap) + lbl.setScaledContents(True) + lbl.show() + def update_result_amount(self, index): amount = int(self.resultAmountCombo.itemText(index)) Config.set(TOTAL_SEARCH_RESULTS, amount) @@ -239,6 +259,7 @@ def init_signals(self): tree.signals.onSelected.connect(self.update_item_labels) tree.signals.doubleClicked.connect(self.on_try_play_item) tree.signals.onListenQueued.connect(self.music_controller.queue_track) + tree.signals.onListenUnqueued.connect(self.music_controller.remove_track) tree.signals.onDownloadQueued.connect(self.download_controller.on_click_download) return_shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Return, tree.tree, @@ -339,32 +360,32 @@ def init_tree_views(self): self.songs_tree.set_header_item( Track("Index", 0, "Title", "Artists", "Album", duration="Duration", release_date="Release Date")) - self.artists_tree = ItemTree(self.artistsTree, lambda artist: QTreeWidgetItem([str(artist.index), artist.name])) + self.artists_tree = ItemTree(self.artistsTree, lambda artist: QTreeWidgetItem([str(artist.index), artist.name]), "artists") self.artists_tree.set_header_item(Artist("Index", 0, "Name")) self.albums_tree = ItemTree(self.albumsTree, lambda album: QTreeWidgetItem([str(album.index), album.title, album.artists, \ - str(album.total_tracks), str(album.release_date)])) + str(album.total_tracks), str(album.release_date)]), "albums") self.albums_tree.set_header_item(Album("Index", 0, "Title", "Artists", "Total Tracks", "Release Date")) self.playlists_tree = ItemTree(self.playlistsTree, lambda playlist: QTreeWidgetItem([str(playlist.index), playlist.title, \ str(playlist.creator), - str(playlist.total_tracks)])) + str(playlist.total_tracks)]), "playlists") self.playlists_tree.set_header_item(Playlist("Index", 0, "Title", "Author", "Total Tracks")) self.download_tree = ItemTree(self.downloadedTree, - lambda track: QTreeWidgetItem([track.title, track.artists, track.album])) - self.download_tree.set_header_item(Track("", 0, "Title", "Artists", "Albums")) + lambda track: QTreeWidgetItem([track.title, track.artists, track.album]), "download") + self.download_tree.set_header_item(Track("", 0, "Title", "Artists", "Album")) self.liked_tree = ItemTree(self.likedTree, - lambda track: QTreeWidgetItem([track.title, track.artists, track.album])) - self.liked_tree.set_header_item(Track("", 0, "Title", "Artists", "Albums")) + lambda track: QTreeWidgetItem([track.title, track.artists, track.album]), "liked") + self.liked_tree.set_header_item(Track("", 0, "Title", "Artists", "Album")) self.liked_tree.load_function = self.init_liked_view self.queue_tree = ItemTree(self.queueTree, - lambda track: QTreeWidgetItem([track.title, track.artists, track.album]), False) - self.queue_tree.set_header_item(Track("", 0, "Title", "Artists", "Albums")) + lambda track: QTreeWidgetItem([track.title, track.artists, track.album]), "queue", False) + self.queue_tree.set_header_item(Track("", 0, "Title", "Artists", "Album")) self.queue_tree.load_function = self.init_queue_view self.search_trees = [self.songs_tree, self.artists_tree, self.albums_tree, self.playlists_tree] diff --git a/source/audio.py b/source/audio.py index 8b8c20f..39bbd20 100644 --- a/source/audio.py +++ b/source/audio.py @@ -5,11 +5,12 @@ import music_tag import logging import struct +import keyboard from pathlib import Path from PyQt5 import QtCore, QtGui, QtTest from PyQt5.QtCore import pyqtSignal, QThreadPool, QObject from PyQt5.QtGui import QImage, QPixmap -from const import COMMENT, ID, ARTWORK, PLAY_ICON, PAUSE_ICON, TRACKTITLE, ARTIST, ALBUM, ARTWORK, FORMATS, \ +from const import COMMENT, ID, ALBUM_ID, ARTWORK, PLAY_ICON, PAUSE_ICON, TRACKTITLE, ARTIST, ALBUM, ARTWORK, FORMATS, \ VOL_ICON, MUTE_ICON, SHUFFLE_ON_ICON, SHUFFLE_OFF_ICON, REPEAT_ON_ICON, REPEAT_OFF_ICON, NEXT_ICON, PREV_ICON,\ LISTEN_QUEUE_ICON from zspotify import ZSpotify @@ -19,6 +20,7 @@ from glob import glob from view import set_button_icon, set_label_image from config import Config +from discordRpc import RPC logger = logging.getLogger(__name__) @@ -34,11 +36,14 @@ def __init__(self, window): self.playlist_tree = None self.shuffle = False self.repeat = False + self.listen_queue = [] self.shuffle_queue = [] self.awaiting_play = False self.queue_next_song = False self.paused = False + self.muted = False + self._volume = 100 self.seeking = False self.worker = None self.audio_player = AudioPlayer(self.update_music_progress) @@ -50,6 +55,13 @@ def __init__(self, window): set_button_icon(self.window.shuffleBtn, SHUFFLE_OFF_ICON) set_button_icon(self.window.repeatBtn, REPEAT_OFF_ICON) set_button_icon(self.window.listenQueueBtn, LISTEN_QUEUE_ICON) + set_button_icon(self.window.volIconBtn, VOL_ICON) + if Config.get_enable_media_keys(): + keyboard.on_press(self.key_pressed) + if not Config.get_relative_time(): + self.window.remainingTimeLabel.setText(self.window._translate("MainWindow", "0:00")) + if Config.get_enable_discord_rpc(): + self.rpc = RPC() def play(self, item, playlist_tree): @@ -71,6 +83,12 @@ def play(self, item, playlist_tree): self.playlist_tree.select_item(item) self.onPlay.emit(item) + if Config.get_enable_discord_rpc(): + try: + self.rpc.set_rpc_to_item(item) + except: + logging.error("Discord RPC failed") + def start_progress_worker(self): self.worker = Worker(self.run_progress_bar, update=self.update_music_progress, signals=MusicSignals()) @@ -80,12 +98,14 @@ def start_progress_worker(self): def pause(self): self.set_button_icon(self.window.playBtn, PLAY_ICON) + self.window.playBtn.setToolTip(self.window._translate("MainWindow", "Play")) self.paused = True self.queue_next_song = False self.audio_player.pause() def unpause(self): self.set_button_icon(self.window.playBtn, PAUSE_ICON) + self.window.playBtn.setToolTip(self.window._translate("MainWindow", "Pause")) self.paused = False self.queue_next_song = True self.audio_player.unpause() @@ -96,7 +116,7 @@ def queue_track(self, item, index=-1): if index == -1: self.listen_queue.append(item) else: - self.listen_queue.insert(index,item) + self.listen_queue.insert(index,item) def remove_track(self, item): if not item: return @@ -111,17 +131,21 @@ def toggle_shuffle(self): if self.shuffle: if self.playlist_tree and self.playlist_tree.items: self.set_button_icon(self.window.shuffleBtn, SHUFFLE_ON_ICON) + self.window.shuffleBtn.setToolTip(self.window._translate("MainWindow", "Disable shuffle")) self.shuffle_queue = self.playlist_tree.items.copy() random.shuffle(self.shuffle_queue) else: self.set_button_icon(self.window.shuffleBtn, SHUFFLE_OFF_ICON) + self.window.shuffleBtn.setToolTip(self.window._translate("MainWindow", "Enable shuffle")) def toggle_repeat(self): self.repeat = not self.repeat if self.repeat: self.set_button_icon(self.window.repeatBtn, REPEAT_ON_ICON) + self.window.repeatBtn.setToolTip(self.window._translate("MainWindow", "Disable repeat")) else: self.set_button_icon(self.window.repeatBtn, REPEAT_OFF_ICON) + self.window.repeatBtn.setToolTip(self.window._translate("MainWindow", "Enable repeat")) def update_music_progress(self, perc, elapsed, total): if not self.seeking: @@ -131,9 +155,9 @@ def update_music_progress(self, perc, elapsed, total): duration = self.audio_player.player.get_length() playback_bar_perc = self.window.playbackBar.value()/self.window.playbackBar.maximum() self.window.elapsedTimeLabel.setText(ms_to_time_str(duration * playback_bar_perc)) - self.window.remainingTimeLabel.setText(f"-{ms_to_time_str(duration * (1-playback_bar_perc))}") + self.window.remainingTimeLabel.setText(f"-{ms_to_time_str(duration * (1-playback_bar_perc))}" if Config.get_relative_time() else ms_to_time_str(int(total))) self.window.elapsedTimeLabel.setText(ms_to_time_str(elapsed)) - self.window.remainingTimeLabel.setText(f"-{ms_to_time_str(int(total)-int(elapsed))}") + self.window.remainingTimeLabel.setText(f"-{ms_to_time_str(int(total)-int(elapsed))}" if Config.get_relative_time() else ms_to_time_str(int(total))) def run_progress_bar(self, signal, *args, **kwargs): while self.audio_player.is_playing() or self.awaiting_play: @@ -149,10 +173,15 @@ def on_progress_finished(self): self.on_next() def set_volume(self, value): + self._volume = value + self.muted = False self.audio_player.set_volume(value) if value == 0: - set_label_image(self.window.volIconLabel, MUTE_ICON) - else: set_label_image(self.window.volIconLabel, VOL_ICON) + self.set_button_icon(self.window.volIconBtn, MUTE_ICON) + self.window.volIconBtn.setToolTip(self.window._translate("MainWindow", "Unmute")) + else: + self.set_button_icon(self.window.volIconBtn, VOL_ICON) + self.window.volIconBtn.setToolTip(self.window._translate("MainWindow", "Mute")) def on_press_play(self): if self.audio_player.is_playing(): @@ -219,10 +248,22 @@ def on_play_queue_song(self): if item in self.shuffle_queue: self.shuffle_queue + def toggle_mute(self): + self.muted = not self.muted + if self.muted: + self.audio_player.set_volume(0) + self.set_button_icon(self.window.volIconBtn, MUTE_ICON) + self.window.volIconBtn.setToolTip(self.window._translate("MainWindow", "Unmute")) + else: + self.audio_player.set_volume(self._volume) + if self._volume != 0: + self.set_button_icon(self.window.volIconBtn, VOL_ICON) + self.window.volIconBtn.setToolTip(self.window._translate("MainWindow", "Mute")) + def update_playing_info(self, item): self.window.playingInfo1.setText(item.title) - self.window.playingInfo1.setToolTip(item.title) + self.window.playingInfo1.setToolTip(f"On album {item.album}") self.window.playingInfo2.setText(item.artists) self.window.playingInfo2.setToolTip(item.artists) @@ -232,7 +273,7 @@ def set_button_icon(self, btn, icon_path): btn.setIcon(icon) def set_vol_icon(self, icon_path): - lbl = self.window.volIconLabel + lbl = self.window.volIconBtn pixmap = QPixmap(icon_path) lbl.setPixmap(pixmap) lbl.setScaledContents(True) @@ -249,6 +290,15 @@ def init_signals(self): self.window.prevBtn.clicked.connect(self.on_prev) self.window.shuffleBtn.clicked.connect(self.toggle_shuffle) self.window.repeatBtn.clicked.connect(self.toggle_repeat) + self.window.volIconBtn.clicked.connect(self.toggle_mute) + + def key_pressed(self, e): + if e.name == "play/pause media": + self.window.playBtn.click() + elif e.name == "next track": + self.window.nextBtn.click() + elif e.name == "previous track": + self.window.prevBtn.click() class AudioPlayer: @@ -339,8 +389,9 @@ def get_track_file_as_item(file, index): meta_data = parse_meta_data(tag[COMMENT]) item_id = meta_data.get(ID) or "" img = meta_data.get(ARTWORK) or "" + album_id = meta_data.get(ALBUM_ID) or "" track = Track(index, item_id, str(tag[TRACKTITLE]), str(tag[ARTIST]), - album=str(tag[ALBUM]), img=img, downloaded=True, path=Path(file)) + album=str(tag[ALBUM]), img=img, album_id=album_id, downloaded=True, path=Path(file)) return track return None @@ -349,3 +400,4 @@ def find_id_in_metadata(path): tag = music_tag.load_file(path) data = parse_meta_data(tag[COMMENT]) return data.get(ID) if None else "" + diff --git a/source/config.py b/source/config.py index 18a74f3..81a8d1c 100644 --- a/source/config.py +++ b/source/config.py @@ -29,6 +29,10 @@ PRINT_ERRORS = 'PRINT_ERRORS' PRINT_DOWNLOADS = 'PRINT_DOWNLOADS' TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR' +ENABLE_MEDIA_KEYS = 'ENABLE_MEDIA_KEYS' +RELATIVE_TIME = 'RELATIVE_TIME' +ENABLE_DISCORD_RPC = 'ENABLE_DISCORD_RPC' +DISCORD_RPC_APP_ID = 'DISCORD_RPC_APP_ID' CONFIG_VALUES = { ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' }, @@ -54,6 +58,10 @@ PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' }, PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' }, TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' }, + ENABLE_MEDIA_KEYS: { 'default': 'True', 'type': bool, 'arg': '--enable-media-keys' }, + RELATIVE_TIME: { 'default': 'True', 'type': bool, 'arg': '--relative-time' }, + ENABLE_DISCORD_RPC: { 'default': 'False', 'type': bool, 'arg': '--enable-discord-rpc' }, + DISCORD_RPC_APP_ID: { 'default': '', 'type': int, 'arg': '--discord-rpc-app-id' } } OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}' @@ -211,6 +219,22 @@ def get_temp_download_dir(cls) -> str: def get_total_search_results(cls): return cls.get(TOTAL_SEARCH_RESULTS) + @classmethod + def get_enable_media_keys(cls): + return cls.get(ENABLE_MEDIA_KEYS) + + @classmethod + def get_relative_time(cls): + return cls.get(RELATIVE_TIME) + + @classmethod + def get_enable_discord_rpc(cls): + return cls.get(ENABLE_DISCORD_RPC) + + @classmethod + def get_discord_rpc_app_id(cls): + return cls.get(DISCORD_RPC_APP_ID) + @classmethod def get_output(cls, mode: str) -> str: v = cls.get(OUTPUT) diff --git a/source/const.py b/source/const.py index 0221ed5..ce2a2a7 100755 --- a/source/const.py +++ b/source/const.py @@ -38,8 +38,16 @@ ID = 'id' +ALBUM_ID = 'album_id' + URL = 'url' +EXTERNAL_URLS = 'external_urls' + +SPOTIFY = 'spotify' + +SPOTIFY_URL = 'https://open.spotify.com/' + RELEASE_DATE = 'release_date' IMAGES = 'images' diff --git a/source/discordRpc.py b/source/discordRpc.py new file mode 100644 index 0000000..bdf4619 --- /dev/null +++ b/source/discordRpc.py @@ -0,0 +1,42 @@ +from pypresence import Presence +import threading +import time +from config import Config + + +class RPC: + def __init__(self): + self.app_id = Config.get_discord_rpc_app_id() + self.rpc = Presence(self.app_id) + self.rpc.connect() + self.set_rpc_activity(**{ + "details": "Not currently playing", + "state": "any music", + "large_image": "zsplogo", + "large_text": "ZSpotify" + }) + + def set_rpc_activity(self, **kwargs): + self.rpc.update(**kwargs) + + def set_rpc_to_item(self, item): + details = "Unknown" + state = "" + if "title" in dir(item): + details = item.title + if "artists" in dir(item): + state = item.artists + if "album" in dir(item): + if "artists" in dir(item): + state += " - " + item.album + else: + state = item.album + if state == "": + state = "Unknown" + + self.set_rpc_activity(**{ + "details": details, + "state": state, + "large_image": "zsplogo", + "large_text": "ZSpotify" + }) diff --git a/source/item.py b/source/item.py index c961027..625762c 100755 --- a/source/item.py +++ b/source/item.py @@ -1,18 +1,21 @@ +from const import TRACK, ALBUM, ARTIST, PLAYLIST, SPOTIFY_URL from utils import set_audio_tags class Item: - def __init__(self, index, downloaded=False, path=""): + def __init__(self, index, downloaded=False, path="", _type="", ID=""): + self.type = _type self.index = index self.downloaded = downloaded self.path = path + self.id = ID if ID else "" + self.url = f"{SPOTIFY_URL}{_type}/{ID}" if ID and _type else "" class Track(Item): - def __init__(self, index, id, title, artists, album="", img="",release_date="", duration=-1, disc_number=-1, - track_number=1, downloaded=False, path=""): - super().__init__(index, downloaded, path) - self.id = id + def __init__(self, index, ID, title, artists, album="", img="", release_date="", duration=-1, disc_number=-1, + track_number=1, album_id="", downloaded=False, path=""): + super().__init__(index, downloaded, path, TRACK, ID) self.title = title self.artists = artists self.album = album @@ -21,17 +24,18 @@ def __init__(self, index, id, title, artists, album="", img="",release_date="", self.duration = duration self.disc_number = disc_number self.track_number = track_number + self.album_id = album_id + self.album_url = "" if self.album_id == "" else f"{SPOTIFY_URL}{ALBUM}/{self.album_id}" def update_meta_tags(self): set_audio_tags(self.path, str(self.artists), self.title, self.album, - disc_number=self.disc_number, track_number=self.track_number, spotify_id=self.id, img=self.img) + disc_number=self.disc_number, track_number=self.track_number, spotify_id=self.id, + album_id=self.album_id, img=self.img) class Album(Item): - def __init__(self, index, id, title,artists, total_tracks, release_date="", img="",downloaded=False, path=""): - super().__init__(index, downloaded, path) - self.index = index - self.id = id + def __init__(self, index, ID, title, artists, total_tracks, release_date="", img="", downloaded=False, path=""): + super().__init__(index, downloaded, path, ALBUM, ID) self.title = title self.artists = artists self.img = img @@ -41,19 +45,17 @@ def __init__(self, index, id, title,artists, total_tracks, release_date="", img= class Artist(Item): - def __init__(self,index, id, name, img="",downloaded=False, path=""): - super().__init__(index, downloaded, path) - self.id = id + def __init__(self,index, ID, name, img="", downloaded=False, path=""): + super().__init__(index, downloaded, path, ARTIST, ID) self.name = name self.img = img self.index = index class Playlist(Item): - def __init__(self, index, id, title,creator, total_tracks, img="", downloaded=False, path=""): - super().__init__(index, downloaded, path) - self.id = id + def __init__(self, index, ID, title, creator, total_tracks, img="", downloaded=False, path=""): + super().__init__(index, downloaded, path, PLAYLIST, ID) self.title = title self.creator = creator self.total_tracks = total_tracks - self.img=img + self.img = img diff --git a/source/itemTree.py b/source/itemTree.py index 7903365..33686c9 100644 --- a/source/itemTree.py +++ b/source/itemTree.py @@ -1,12 +1,15 @@ +import pyperclip from PyQt5.QtWidgets import QTreeWidgetItem, QMenu, QApplication, QTreeWidget from PyQt5.QtCore import Qt, pyqtSignal, QObject +from const import TRACK from item import Item from utils import delete_file class ItemTree: - def __init__(self, tree, tree_widget_builder = None, can_play = True): + def __init__(self, tree, tree_widget_builder = None, name = "", can_play = True): self.tree = tree + self.name = name self.items = [] self.tree_items = {} self.selected_item = None @@ -31,8 +34,10 @@ def add_item(self, item): self.items.append(item) self.tree_items[item] = widget_item self.tree.addTopLevelItem(widget_item) + self.update_index(item) def remove_item(self, item): + if item not in self.items: return self.items.remove(item) if item in self.tree_items: index = self.item_index(item) @@ -84,6 +89,9 @@ def item_index(self, item): return i return -1 + def update_index(self, item): + item.index = self.item_index(item) + def current_item_index(self): item = self.get_selected_item() if not item: return -1 @@ -96,6 +104,7 @@ def clear(self): self.tree.clear() + # FIXME: doesn't work properly if multiple of same entry def get_selected_item(self): tree_widget = self.tree.currentItem() if tree_widget: @@ -148,16 +157,36 @@ def on_download_item(self): def on_listen_queue(self): if not self.selected_item: return + if self.name == "queue": + self.add_item(self.selected_item) self.signals.onListenQueued.emit(self.selected_item) + def on_listen_unqueue(self): + if not self.selected_item: return + self.signals.onListenUnqueued.emit(self.selected_item) + self.remove_item(self.selected_item) + + def on_copy_link(self): + pyperclip.copy(self.selected_item.url) + + def on_copy_album_link(self): + pyperclip.copy(self.selected_item.album_url) + def on_context_menu(self, pos): if not self.selected_item: return + #self.selected_item.update_meta_tags() node = self.tree.mapToGlobal(pos) self.popup_menu = QMenu(None) if self.selected_item.downloaded: self.popup_menu.addAction("Add to listen queue", self.on_listen_queue) + if self.name == "queue": + self.popup_menu.addAction("Remove from listen queue", self.on_listen_unqueue) else: - self.popup_menu.addAction("Add to download queue", self.on_download_item) + self.popup_menu.addAction("Add to download queue", self.on_download_item) + if self.selected_item.url != "": + self.popup_menu.addAction("Copy link", self.on_copy_link) + if self.selected_item.type == TRACK and self.selected_item.album_url != "": + self.popup_menu.addAction("Copy album link", self.on_copy_album_link) if self.selected_item.downloaded: self.popup_menu.addSeparator() self.popup_menu.addAction("Delete", self.on_delete_item) @@ -169,10 +198,12 @@ def init_signals(self): self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.on_context_menu) + class ItemTreeSignals(QObject): doubleClicked = pyqtSignal(Item, ItemTree) itemChanged = pyqtSignal(Item, list, list) onSelected = pyqtSignal(list) onDeleted = pyqtSignal(Item, QTreeWidget) onListenQueued = pyqtSignal(Item) + onListenUnqueued = pyqtSignal(Item) onDownloadQueued = pyqtSignal(Item) diff --git a/source/main_window.py b/source/main_window.py index 6fc478d..e2fa174 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -12,6 +12,15 @@ class Ui_MainWindow(object): + def __init__(self): + self._translate = QtCore.QCoreApplication.translate + + self.largeFont = QtGui.QFont() + self.largeFont.setPointSize(11) + self.largeBoldFont = QtGui.QFont() + self.largeBoldFont.setPointSize(11) + self.largeBoldFont.setBold(True) + def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(1440, 855) @@ -84,13 +93,14 @@ def setupUi(self, MainWindow): self.musicTabs = QtWidgets.QTabWidget(self.centralwidget) self.musicTabs.setStyleSheet("QTabWidget::pane\n" "{\n" -" border:0px;\n" +" border: 0px;\n" "}\n" "\n" "QTabWidget::tab-bar\n" "{\n" " alignment: center;\n" "}") + self.musicTabs.setFont(self.largeFont) self.musicTabs.setTabPosition(QtWidgets.QTabWidget.West) self.musicTabs.setTabBarAutoHide(True) self.musicTabs.setObjectName("musicTabs") @@ -104,6 +114,7 @@ def setupUi(self, MainWindow): "{\n" " alignment: center;\n" "}") + self.libraryTabs.setFont(self.largeFont) self.libraryTabs.setObjectName("libraryTabs") self.downloadedTab = QtWidgets.QWidget() self.downloadedTab.setObjectName("downloadedTab") @@ -141,6 +152,7 @@ def setupUi(self, MainWindow): self.verticalLayout_15.setContentsMargins(1, 0, 1, 1) self.verticalLayout_15.setObjectName("verticalLayout_15") self.searchTabs = QtWidgets.QTabWidget(self.resultLayout) + self.searchTabs.setFont(self.largeFont) self.searchTabs.setStyleSheet("") self.searchTabs.setTabPosition(QtWidgets.QTabWidget.North) self.searchTabs.setElideMode(QtCore.Qt.ElideRight) @@ -212,6 +224,7 @@ def setupUi(self, MainWindow): self.verticalLayout_7.setContentsMargins(3, 0, 3, 3) self.verticalLayout_7.setObjectName("verticalLayout_7") self.queueTabs = QtWidgets.QTabWidget(self.queueTabLayout) + self.queueTabs.setFont(self.largeFont) self.queueTabs.setObjectName("queueTabs") self.queueTreeTab = QtWidgets.QWidget() self.queueTreeTab.setObjectName("queueTreeTab") @@ -245,12 +258,14 @@ def setupUi(self, MainWindow): self.playingInfo1.setMinimumSize(QtCore.QSize(250, 0)) self.playingInfo1.setMaximumSize(QtCore.QSize(250, 16777215)) self.playingInfo1.setText("") + self.playingInfo1.setFont(self.largeBoldFont) self.playingInfo1.setObjectName("playingInfo1") self.verticalLayout_6.addWidget(self.playingInfo1) self.playingInfo2 = QtWidgets.QLabel(self.mediaControls) self.playingInfo2.setMinimumSize(QtCore.QSize(250, 0)) self.playingInfo2.setMaximumSize(QtCore.QSize(250, 16777215)) self.playingInfo2.setText("") + self.playingInfo2.setFont(self.largeFont) self.playingInfo2.setObjectName("playingInfo2") self.verticalLayout_6.addWidget(self.playingInfo2) self.horizontalLayout_5.addLayout(self.verticalLayout_6) @@ -270,6 +285,7 @@ def setupUi(self, MainWindow): self.repeatBtn.setIcon(icon) self.repeatBtn.setFlat(True) self.repeatBtn.setObjectName("repeatBtn") + self.repeatBtn.setToolTip(self._translate("MainWindow", "Enable repeat")) self.horizontalLayout_7.addWidget(self.repeatBtn) self.horizontalLayout_8 = QtWidgets.QHBoxLayout() self.horizontalLayout_8.setContentsMargins(-1, -1, 0, -1) @@ -284,6 +300,7 @@ def setupUi(self, MainWindow): self.prevBtn.setIconSize(QtCore.QSize(16, 16)) self.prevBtn.setFlat(True) self.prevBtn.setObjectName("prevBtn") + self.prevBtn.setToolTip(self._translate("MainWindow", "Previous")) self.horizontalLayout_8.addWidget(self.prevBtn) self.verticalLayout_5 = QtWidgets.QVBoxLayout() self.verticalLayout_5.setContentsMargins(0, -1, -1, 20) @@ -299,6 +316,7 @@ def setupUi(self, MainWindow): self.playBtn.setIconSize(QtCore.QSize(24, 24)) self.playBtn.setFlat(True) self.playBtn.setObjectName("playBtn") + self.playBtn.setToolTip(self._translate("MainWindow", "Play")) self.verticalLayout_5.addWidget(self.playBtn) self.horizontalLayout_8.addLayout(self.verticalLayout_5) self.nextBtn = QtWidgets.QPushButton(self.mediaControls) @@ -310,6 +328,7 @@ def setupUi(self, MainWindow): self.nextBtn.setIconSize(QtCore.QSize(16, 16)) self.nextBtn.setFlat(True) self.nextBtn.setObjectName("nextBtn") + self.nextBtn.setToolTip(self._translate("MainWindow", "Next")) self.horizontalLayout_8.addWidget(self.nextBtn) self.horizontalLayout_7.addLayout(self.horizontalLayout_8) self.shuffleBtn = QtWidgets.QPushButton(self.mediaControls) @@ -320,6 +339,7 @@ def setupUi(self, MainWindow): self.shuffleBtn.setIconSize(QtCore.QSize(16, 16)) self.shuffleBtn.setFlat(True) self.shuffleBtn.setObjectName("shuffleBtn") + self.shuffleBtn.setToolTip(self._translate("MainWindow", "Enable shuffle")) self.horizontalLayout_7.addWidget(self.shuffleBtn) spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_7.addItem(spacerItem3) @@ -365,16 +385,18 @@ def setupUi(self, MainWindow): self.listenQueueBtn.setIconSize(QtCore.QSize(20, 20)) self.listenQueueBtn.setFlat(True) self.listenQueueBtn.setObjectName("listenQueueBtn") + self.listenQueueBtn.setToolTip(self._translate("MainWindow", "Queue")) self.horizontalLayout_5.addWidget(self.listenQueueBtn) - self.volIconLabel = QtWidgets.QLabel(self.mediaControls) - self.volIconLabel.setMinimumSize(QtCore.QSize(16, 16)) - self.volIconLabel.setMaximumSize(QtCore.QSize(16, 16)) - self.volIconLabel.setText("") - self.volIconLabel.setTextFormat(QtCore.Qt.RichText) - self.volIconLabel.setPixmap(QtGui.QPixmap("Resources/volIcon.png")) - self.volIconLabel.setScaledContents(True) - self.volIconLabel.setObjectName("volIconLabel") - self.horizontalLayout_5.addWidget(self.volIconLabel) + self.volIconBtn = QtWidgets.QPushButton(self.mediaControls) + self.volIconBtn.setText("") + icon6 = QtGui.QIcon() + icon6.addPixmap(QtGui.QPixmap("Resources/volIcon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.volIconBtn.setIcon(icon6) + self.volIconBtn.setIconSize(QtCore.QSize(16, 16)) + self.volIconBtn.setFlat(True) + self.volIconBtn.setObjectName("volIconBtn") + self.volIconBtn.setToolTip(self._translate("MainWindow", "Mute")) + self.horizontalLayout_5.addWidget(self.volIconBtn) self.label_7 = QtWidgets.QLabel(self.mediaControls) self.label_7.setText("") self.label_7.setTextFormat(QtCore.Qt.RichText) @@ -611,9 +633,9 @@ def setupUi(self, MainWindow): self.dirBtn.setMinimumSize(QtCore.QSize(30, 0)) self.dirBtn.setMaximumSize(QtCore.QSize(40, 40)) self.dirBtn.setText("") - icon6 = QtGui.QIcon() - icon6.addPixmap(QtGui.QPixmap("Resources/folderIcon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.dirBtn.setIcon(icon6) + icon7 = QtGui.QIcon() + icon7.addPixmap(QtGui.QPixmap("Resources/folderIcon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.dirBtn.setIcon(icon7) self.dirBtn.setObjectName("dirBtn") self.horizontalLayout_4.addWidget(self.dirBtn) self.horizontalLayout_4.setStretch(1, 3) @@ -633,60 +655,61 @@ def setupUi(self, MainWindow): QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "ZSpotify")) - self.searchInput.setPlaceholderText(_translate("MainWindow", "Search Spotify")) - self.searchBtn.setText(_translate("MainWindow", "Search")) - self.resultAmountCombo.setItemText(0, _translate("MainWindow", "10")) - self.resultAmountCombo.setItemText(1, _translate("MainWindow", "20")) - self.resultAmountCombo.setItemText(2, _translate("MainWindow", "30")) - self.resultAmountCombo.setItemText(3, _translate("MainWindow", "40")) - self.resultAmountCombo.setItemText(4, _translate("MainWindow", "50")) - self.label_6.setText(_translate("MainWindow", "Results")) - self.loginBtn.setText(_translate("MainWindow", "Login")) + MainWindow.setWindowTitle(self._translate("MainWindow", "ZSpotify")) + MainWindow.setWindowIcon(QtGui.QIcon("Resources/ZSpot_Icon.png")) + self.searchInput.setPlaceholderText(self._translate("MainWindow", "Search Spotify")) + self.searchBtn.setText(self._translate("MainWindow", "Search")) + self.resultAmountCombo.setItemText(0, self._translate("MainWindow", "10")) + self.resultAmountCombo.setItemText(1, self._translate("MainWindow", "20")) + self.resultAmountCombo.setItemText(2, self._translate("MainWindow", "30")) + self.resultAmountCombo.setItemText(3, self._translate("MainWindow", "40")) + self.resultAmountCombo.setItemText(4, self._translate("MainWindow", "50")) + self.label_6.setText(self._translate("MainWindow", "Results")) + self.loginBtn.setText(self._translate("MainWindow", "Login")) self.downloadedTree.setSortingEnabled(True) - self.downloadedTree.headerItem().setText(1, _translate("MainWindow", "Artists")) - self.downloadedTree.headerItem().setText(2, _translate("MainWindow", "Album")) - self.libraryTabs.setTabText(self.libraryTabs.indexOf(self.downloadedTab), _translate("MainWindow", "Downloaded")) + self.downloadedTree.headerItem().setText(1, self._translate("MainWindow", "Artists")) + self.downloadedTree.headerItem().setText(2, self._translate("MainWindow", "Album")) + self.libraryTabs.setTabText(self.libraryTabs.indexOf(self.downloadedTab), self._translate("MainWindow", "Downloaded")) self.likedTree.setSortingEnabled(True) - self.libraryTabs.setTabText(self.libraryTabs.indexOf(self.likedTab), _translate("MainWindow", "Liked")) - self.musicTabs.setTabText(self.musicTabs.indexOf(self.libraryLayout), _translate("MainWindow", "Your Library")) - self.songsTree.headerItem().setText(0, _translate("MainWindow", "Index")) - self.songsTree.headerItem().setText(1, _translate("MainWindow", "Title")) - self.songsTree.headerItem().setText(2, _translate("MainWindow", "Artists")) - self.songsTree.headerItem().setText(3, _translate("MainWindow", "Album")) - self.songsTree.headerItem().setText(4, _translate("MainWindow", "Duration")) - self.songsTree.headerItem().setText(5, _translate("MainWindow", "Release Date")) - self.searchTabs.setTabText(self.searchTabs.indexOf(self.songsTab), _translate("MainWindow", "Songs")) - self.artistsTree.headerItem().setText(0, _translate("MainWindow", "Index")) - self.artistsTree.headerItem().setText(1, _translate("MainWindow", "Name")) - self.searchTabs.setTabText(self.searchTabs.indexOf(self.artistsTab), _translate("MainWindow", "Artists")) - self.albumsTree.headerItem().setText(0, _translate("MainWindow", "Index")) - self.albumsTree.headerItem().setText(1, _translate("MainWindow", "Title")) - self.albumsTree.headerItem().setText(2, _translate("MainWindow", "Artists")) - self.albumsTree.headerItem().setText(3, _translate("MainWindow", "Total Tracks")) - self.albumsTree.headerItem().setText(4, _translate("MainWindow", "Release Date")) - self.searchTabs.setTabText(self.searchTabs.indexOf(self.albumsTab), _translate("MainWindow", "Albums")) - self.playlistsTree.headerItem().setText(0, _translate("MainWindow", "Index")) - self.playlistsTree.headerItem().setText(1, _translate("MainWindow", "Title")) - self.playlistsTree.headerItem().setText(2, _translate("MainWindow", "Creator")) - self.playlistsTree.headerItem().setText(3, _translate("MainWindow", "Total Tracks")) - self.searchTabs.setTabText(self.searchTabs.indexOf(self.playlistsTab), _translate("MainWindow", "Playlists")) - self.musicTabs.setTabText(self.musicTabs.indexOf(self.resultLayout), _translate("MainWindow", "Search Results")) - self.queueTree.headerItem().setText(0, _translate("MainWindow", "Title")) - self.queueTree.headerItem().setText(1, _translate("MainWindow", "Artists")) - self.queueTree.headerItem().setText(2, _translate("MainWindow", "Album")) - self.queueTabs.setTabText(self.queueTabs.indexOf(self.queueTreeTab), _translate("MainWindow", "Queue")) - self.musicTabs.setTabText(self.musicTabs.indexOf(self.queueTabLayout), _translate("MainWindow", "Queue")) - self.elapsedTimeLabel.setText(_translate("MainWindow", "0:00")) - self.remainingTimeLabel.setText(_translate("MainWindow", "-0:00")) - self.dlQualityHeader.setText(_translate("MainWindow", "Download Quality:")) - self.dlQualityLabel.setText(_translate("MainWindow", "320kbps")) - self.fileFormatHeader.setText(_translate("MainWindow", "File Format:")) - self.fileFormatCombo.setItemText(0, _translate("MainWindow", ".mp3")) - self.fileFormatCombo.setItemText(1, _translate("MainWindow", ".ogg")) - self.label_5.setText(_translate("MainWindow", "Download in Real Time:")) - self.label_2.setText(_translate("MainWindow", "Download Queue:")) - self.downloadBtn.setText(_translate("MainWindow", "Download ")) - self.dirBtn.setToolTip(_translate("MainWindow", "Change download directory")) + self.libraryTabs.setTabText(self.libraryTabs.indexOf(self.likedTab), self._translate("MainWindow", "Liked")) + self.musicTabs.setTabText(self.musicTabs.indexOf(self.libraryLayout), self._translate("MainWindow", "Your Library")) + self.songsTree.headerItem().setText(0, self._translate("MainWindow", "Index")) + self.songsTree.headerItem().setText(1, self._translate("MainWindow", "Title")) + self.songsTree.headerItem().setText(2, self._translate("MainWindow", "Artists")) + self.songsTree.headerItem().setText(3, self._translate("MainWindow", "Album")) + self.songsTree.headerItem().setText(4, self._translate("MainWindow", "Duration")) + self.songsTree.headerItem().setText(5, self._translate("MainWindow", "Release Date")) + self.searchTabs.setTabText(self.searchTabs.indexOf(self.songsTab), self._translate("MainWindow", "Songs")) + self.artistsTree.headerItem().setText(0, self._translate("MainWindow", "Index")) + self.artistsTree.headerItem().setText(1, self._translate("MainWindow", "Name")) + self.searchTabs.setTabText(self.searchTabs.indexOf(self.artistsTab), self._translate("MainWindow", "Artists")) + self.albumsTree.headerItem().setText(0, self._translate("MainWindow", "Index")) + self.albumsTree.headerItem().setText(1, self._translate("MainWindow", "Title")) + self.albumsTree.headerItem().setText(2, self._translate("MainWindow", "Artists")) + self.albumsTree.headerItem().setText(3, self._translate("MainWindow", "Total Tracks")) + self.albumsTree.headerItem().setText(4, self._translate("MainWindow", "Release Date")) + self.searchTabs.setTabText(self.searchTabs.indexOf(self.albumsTab), self._translate("MainWindow", "Albums")) + self.playlistsTree.headerItem().setText(0, self._translate("MainWindow", "Index")) + self.playlistsTree.headerItem().setText(1, self._translate("MainWindow", "Title")) + self.playlistsTree.headerItem().setText(2, self._translate("MainWindow", "Creator")) + self.playlistsTree.headerItem().setText(3, self._translate("MainWindow", "Total Tracks")) + self.searchTabs.setTabText(self.searchTabs.indexOf(self.playlistsTab), self._translate("MainWindow", "Playlists")) + self.musicTabs.setTabText(self.musicTabs.indexOf(self.resultLayout), self._translate("MainWindow", "Search Results")) + self.queueTree.headerItem().setText(0, self._translate("MainWindow", "Title")) + self.queueTree.headerItem().setText(1, self._translate("MainWindow", "Artists")) + self.queueTree.headerItem().setText(2, self._translate("MainWindow", "Album")) + self.queueTabs.setTabText(self.queueTabs.indexOf(self.queueTreeTab), self._translate("MainWindow", "Queue")) + self.musicTabs.setTabText(self.musicTabs.indexOf(self.queueTabLayout), self._translate("MainWindow", "Queue")) + self.elapsedTimeLabel.setText(self._translate("MainWindow", "0:00")) + self.remainingTimeLabel.setText(self._translate("MainWindow", "-0:00")) + self.dlQualityHeader.setText(self._translate("MainWindow", "Download Quality:")) + self.dlQualityLabel.setText(self._translate("MainWindow", "320kbps")) + self.fileFormatHeader.setText(self._translate("MainWindow", "File Format:")) + self.fileFormatCombo.setItemText(0, self._translate("MainWindow", ".mp3")) + self.fileFormatCombo.setItemText(1, self._translate("MainWindow", ".ogg")) + self.label_5.setText(self._translate("MainWindow", "Download in Real Time:")) + self.label_2.setText(self._translate("MainWindow", "Download Queue:")) + self.downloadBtn.setText(self._translate("MainWindow", "Download ")) + self.dirBtn.setToolTip(self._translate("MainWindow", "Change download directory")) + from view import SeekableSlider diff --git a/source/main_window.ui b/source/main_window.ui index 0f5e7c1..6ab374c 100644 --- a/source/main_window.ui +++ b/source/main_window.ui @@ -1125,29 +1125,21 @@ QTabWidget::tab-bar - - - - 16 - 16 - + + + - + + + Resources/volIcon.pngResources/volIcon.png + + 16 16 - - - - - Qt::RichText - - - Resources/volIcon.png - - + true diff --git a/source/requirements.txt b/source/requirements.txt new file mode 100644 index 0000000..39efc8c --- /dev/null +++ b/source/requirements.txt @@ -0,0 +1,16 @@ +ffmpy +git+https://github.com/kokarare1212/librespot-python +music_tag +Pillow +protobuf +tabulate +PyQt5 +pyqtdarktheme +python-vlc +requests +tqdm +pydub +keyboard +mutagen +pyperclip +git+https://github.com/qwertyquerty/pypresence \ No newline at end of file diff --git a/source/track.py b/source/track.py index 066b69d..c8ebd5b 100755 --- a/source/track.py +++ b/source/track.py @@ -67,8 +67,9 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any scraped_song_id = info[TRACKS][0][ID] is_playable = info[TRACKS][0][IS_PLAYABLE] duration_ms = info[TRACKS][0][DURATION_MS] + album_id = info[TRACKS][0][ALBUM][ID] - return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms + return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms, album_id except Exception as e: raise ValueError(f'Failed to parse TRACKS_URL response: {str(e)}\n{raw}') @@ -121,7 +122,7 @@ def download_track(track_id: str, extra_keys='', prefix=False, prefix_value='', try: logger.info(f"Initialising download {track_id}.") (artists, album_name, name, image_url, release_year, disc_number, - track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id) + track_number, scraped_song_id, is_playable, duration_ms, album_id) = get_song_info(track_id) logger.info(f"Scraped track info.") song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) @@ -139,6 +140,7 @@ def download_track(track_id: str, extra_keys='', prefix=False, prefix_value='', output_template = output_template.replace("{track_number}", fix_filename(track_number)) output_template = output_template.replace("{id}", fix_filename(scraped_song_id)) output_template = output_template.replace("{track_id}", fix_filename(track_id)) + output_template = output_template.replace("{album_id}", fix_filename(album_id)) output_template = output_template.replace("{ext}", ext) filename = os.path.join(Config.get_root_path(), output_template) @@ -222,7 +224,7 @@ def download_track(track_id: str, extra_keys='', prefix=False, prefix_value='', convert_audio_format(filename_temp) logger.info("Setting track metadata.") - set_audio_tags(filename_temp, artists, name, album_name, release_year, disc_number, track_number, spotify_id=scraped_song_id, img=image_url) + set_audio_tags(filename_temp, artists, name, album_name, release_year, disc_number, track_number, spotify_id=scraped_song_id, album_id=album_id, img=image_url) logger.info("Setting track thumbnail.") set_music_thumbnail(filename_temp, image_url) diff --git a/source/utils.py b/source/utils.py index 78ac368..fbf0b80 100755 --- a/source/utils.py +++ b/source/utils.py @@ -12,7 +12,7 @@ import requests from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ - WINDOWS_SYSTEM, ALBUMARTIST, COMMENT, ID + WINDOWS_SYSTEM, ALBUMARTIST, COMMENT, ID, ALBUM_ID from config import Config logger = logging.getLogger(__name__) @@ -125,7 +125,7 @@ def clear() -> None: os.system('clear') -def set_audio_tags(filename, artists, name, album_name, release_year=-1, disc_number=-1, track_number=-1, spotify_id="", img="") -> None: +def set_audio_tags(filename, artists, name, album_name, release_year=-1, disc_number=-1, track_number=-1, spotify_id="", album_id="", img="") -> None: """ sets music_tag metadata """ tags = music_tag.load_file(filename) tags[ALBUMARTIST] = artists[0] @@ -138,6 +138,7 @@ def set_audio_tags(filename, artists, name, album_name, release_year=-1, disc_nu meta_data = {} if not spotify_id == "": meta_data[ID] = spotify_id if not img == "": meta_data[ARTWORK] = img + if not album_id == "": meta_data[ALBUM_ID] = album_id tags[COMMENT] = format_meta_data(meta_data) tags.save() diff --git a/source/zspotify.py b/source/zspotify.py index 330fb3c..af841e7 100755 --- a/source/zspotify.py +++ b/source/zspotify.py @@ -18,7 +18,8 @@ from const import TYPE, ITEMS, \ USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, PREMIUM,\ PLAYLIST_READ_PRIVATE,TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALBUM, ALBUMS, \ - OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME, IMAGES, URL, TOTAL_TRACKS, TOTAL, RELEASE_DATE, USER_LIBRARY_READ, DURATION + OWNER, PLAYLIST, PLAYLISTS, DISPLAY_NAME, IMAGES, URL, TOTAL_TRACKS, TOTAL, RELEASE_DATE, \ + USER_LIBRARY_READ, DURATION, EXTERNAL_URLS, SPOTIFY from utils import MusicFormat, ms_to_time_str from item import Track, Album, Artist, Playlist from config import Config @@ -92,10 +93,13 @@ def search(cls, search_terms): for t in resp[TRACKS][ITEMS]: try: artists = ', '.join([artist[NAME] for artist in t[ARTISTS]]) - url = t[ALBUM][IMAGES][1][URL] + #if SPOTIFY in t[EXTERNAL_URLS]: + #url = t[EXTERNAL_URLS][SPOTIFY] + #else: url = "" + image_url = t[ALBUM][IMAGES][1][URL] duration = ms_to_time_str(t[DURATION]) track = Track(counter, t[ID], str(t[NAME]), artists, str(t[ALBUM][NAME]), \ - release_date=t[ALBUM][RELEASE_DATE], duration=duration, img=url) + release_date=t[ALBUM][RELEASE_DATE], duration=duration, album_id=t[ALBUM][ID], img=image_url)#, url=url) results[TRACKS].append(track) counter += 1 except Exception as e: @@ -105,11 +109,10 @@ def search(cls, search_terms): for a in resp[ALBUMS][ITEMS]: try: if len(a[IMAGES]) > 1: - url = a[IMAGES][1][URL] - else: - url = "" + image_url = a[IMAGES][1][URL] + else: image_url = "" artists = ', '.join([artist[NAME] for artist in a[ARTISTS]]) - album = Album(counter, a[ID], a[NAME], artists, a[TOTAL_TRACKS], release_date=a[RELEASE_DATE], img=url) + album = Album(counter, a[ID], a[NAME], artists, a[TOTAL_TRACKS], release_date=a[RELEASE_DATE], img=image_url) results[ALBUMS].append(album) counter += 1 except Exception as e: @@ -119,9 +122,9 @@ def search(cls, search_terms): for ar in resp[ARTISTS][ITEMS]: try: if len(ar[IMAGES]) >= 1: - url = ar[IMAGES][1][URL] - else: url = "" - artist = Artist(counter, ar[ID], ar[NAME], img=url) + image_url = ar[IMAGES][1][URL] + else: image_url = "" + artist = Artist(counter, ar[ID], ar[NAME], img=image_url) results[ARTISTS].append(artist) counter += 1 except Exception as e: @@ -131,10 +134,9 @@ def search(cls, search_terms): for playlist in resp[PLAYLISTS][ITEMS]: try: if len(playlist[IMAGES]) > 0: - url = playlist[IMAGES][0][URL] - else: - url = "" - playlist = Playlist(counter, playlist[ID], playlist[NAME], playlist[OWNER][DISPLAY_NAME], playlist[TRACKS][TOTAL], img=url) + image_url = playlist[IMAGES][0][URL] + else: image_url = "" + playlist = Playlist(counter, playlist[ID], playlist[NAME], playlist[OWNER][DISPLAY_NAME], playlist[TRACKS][TOTAL], img=image_url) results[PLAYLISTS].append(playlist) counter += 1 except Exception as e: @@ -151,7 +153,7 @@ def load_tracks_url(cls, url): artists = ', '.join([artist[NAME] for artist in item[TRACK][ARTISTS]]) duration = ms_to_time_str(item[TRACK][DURATION]) track = Track(index, item[TRACK][ID], item[TRACK][NAME], artists, album=item[TRACK][ALBUM][NAME], \ - img=item[TRACK][ALBUM][IMAGES][1][URL], duration=duration) + img=item[TRACK][ALBUM][IMAGES][1][URL], duration=duration, album_id=item[TRACK][ALBUM][ID]) tracks.append(track) return tracks