Change requirements

This commit is contained in:
unknown
2025-11-22 03:43:40 +01:00
parent 4821831e6f
commit 0d0bd0f6ce
9 changed files with 128 additions and 1065 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2325,7 +2325,7 @@ class Session(Closeable, MessageListener, SubListener):
# Joining from within the same thread would deadlock, so
# guard against that.
if threading.current_thread() is not self.__thread:
self.__thread.join(timeout=1)
self.__thread.join(timeout=0.5)
except Exception:
# Shutdown should be best-effort; if join fails, we
# still proceed with closing the session.
@@ -2368,9 +2368,9 @@ class Session(Closeable, MessageListener, SubListener):
# session keeps reconnecting in a loop after the work is
# finished and the caller expects shutdown.
if not self.__running:
self.__session.logger.info(
"Receiver stopping after connection error: %s", ex
)
#self.__session.logger.info(
# "Receiver stopping after connection error: %s", ex
#)
break
# Detect repeated "connection reset by peer" errors.
@@ -2384,9 +2384,9 @@ class Session(Closeable, MessageListener, SubListener):
# happen when Session.close() has torn down the
# connection while the receiver was blocked in recv().
if isinstance(ex, OSError) and getattr(ex, "errno", None) == 9:
#self.__session.logger.info(
# "Receiver stopping after socket close (errno 9)"
#)
self.__session.logger.info(
"Receiver stopping after socket close (errno 9)"
)
self.__running = False
break
if is_reset:

View File

@@ -1,5 +1,4 @@
ffmpy
https://github.com/kokarare1212/librespot-python/archive/refs/heads/rewrite.zip
music_tag
Pillow
protobuf

View File

@@ -25,30 +25,52 @@ def get_album_tracks(album_id):
def get_album_name(album_id):
""" Returns album name """
(raw, resp) = Zotify.invoke_url(f'{ALBUM_URL}/{album_id}')
return resp[ARTISTS][0][NAME], fix_filename(resp[NAME])
"""Return album's primary artist name and album title, honoring configured locale.
While album/track filenames are now derived from per-track localized metadata,
we still fetch the album here with locale so any other use remains consistent.
"""
locale = Zotify.CONFIG.get_locale()
(raw, resp) = Zotify.invoke_url(f'{ALBUM_URL}/{album_id}?market=from_token&locale={locale}')
return resp[ARTISTS][0][NAME], fix_filename(resp[NAME]) # type: ignore[index]
def get_artist_albums(artist_id):
""" Returns artist's albums """
(raw, resp) = Zotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums?include_groups=album%2Csingle')
# Return a list each album's id
album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]
album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] # type: ignore[index]
# Recursive requests to get all albums including singles an EPs
while resp['next'] is not None:
(raw, resp) = Zotify.invoke_url(resp['next'])
album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))])
album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]) # type: ignore[index]
return album_ids
def download_album(album):
""" Downloads songs from an album """
artist, album_name = get_album_name(album)
""" Downloads songs from an album.
NOTE: We intentionally do NOT pass artist/album names via extra_keys anymore so that the
placeholders {artist} and {album} remain in the output template until the per-track
metadata (queried with the configured locale) is applied inside download_track().
This fixes an issue where album downloads produced filenames with non-localized
artist names (e.g. 'Eason Chan') while single track downloads correctly used the
localized variant (e.g. '陳奕迅'). By letting download_track fill these placeholders
after fetching each track's locale-aware metadata, filenames are now consistent.
"""
# Still fetch once so we trigger an API call early (may warm caches) but we no longer
# inject these values into the template; track-level localized metadata will be used.
get_album_name(album)
tracks = get_album_tracks(album)
for n, track in Printer.progress(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)):
download_track('album', track[ID], extra_keys={'album_num': str(n).zfill(2), 'artist': artist, 'album': album_name, 'album_id': album}, disable_progressbar=True)
# Only pass dynamic numbering and album_id (useful for custom templates using {album_id}).
download_track(
'album',
track[ID],
extra_keys={'album_num': str(n).zfill(2), 'album_id': album},
disable_progressbar=True
)
def download_artist_albums(artist):

View File

@@ -54,10 +54,11 @@ def client(args) -> None:
if args.liked_songs:
for song in get_saved_tracks():
if not song[TRACK][NAME] or not song[TRACK][ID]:
track_obj = song.get(TRACK) if isinstance(song, dict) else None
if not track_obj or not track_obj.get(NAME) or not track_obj.get(ID):
Printer.print(PrintChannel.SKIPS, '### SKIPPING: SONG DOES NOT EXIST ANYMORE ###' + "\n")
else:
download_track('liked', song[TRACK][ID])
continue
download_track('liked', track_obj[ID])
return
if args.followed_artists:
@@ -105,19 +106,20 @@ def download_from_urls(urls: list[str]) -> bool:
enum = 1
char_num = len(str(len(playlist_songs)))
for song in playlist_songs:
if not song[TRACK][NAME] or not song[TRACK][ID]:
track_obj = song.get(TRACK) if isinstance(song, dict) else None
if not track_obj or not track_obj.get(NAME) or not track_obj.get(ID):
Printer.print(PrintChannel.SKIPS, '### SKIPPING: SONG DOES NOT EXIST ANYMORE ###' + "\n")
else:
if song[TRACK][TYPE] == "episode": # Playlist item is a podcast episode
download_episode(song[TRACK][ID])
if track_obj.get(TYPE) == "episode": # Playlist item is a podcast episode
download_episode(track_obj[ID])
else:
download_track('playlist', song[TRACK][ID], extra_keys=
download_track('playlist', track_obj[ID], extra_keys=
{
'playlist_song_name': song[TRACK][NAME],
'playlist_song_name': track_obj[NAME],
'playlist': name,
'playlist_num': str(enum).zfill(char_num),
'playlist_id': playlist_id,
'playlist_track_id': song[TRACK][ID]
'playlist_track_id': track_obj[ID]
})
enum += 1
elif episode_id is not None:

View File

@@ -36,6 +36,13 @@ PRINT_WARNINGS = 'PRINT_WARNINGS'
RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
CONFIG_VERSION = 'CONFIG_VERSION'
DOWNLOAD_LYRICS = 'DOWNLOAD_LYRICS'
LYRICS_LOCATION = 'LYRICS_LOCATION'
LYRICS_FILENAME = 'LYRICS_FILENAME'
ALWAYS_CHECK_LYRICS = 'ALWAYS_CHECK_LYRICS'
LYRICS_MD_HEADER = 'LYRICS_MD_HEADER'
MD_SAVE_LYRICS = 'MD_SAVE_LYRICS'
UNIQUE_LYRICS_FILE = 'UNIQUE_LYRICS_FILE'
LOCALE = 'LOCALE'
CONFIG_VALUES = {
SAVE_CREDENTIALS: { 'default': 'True', 'type': bool, 'arg': '--save-credentials' },
@@ -46,6 +53,12 @@ CONFIG_VALUES = {
ROOT_PODCAST_PATH: { 'default': '', 'type': str, 'arg': '--root-podcast-path' },
SPLIT_ALBUM_DISCS: { 'default': 'False', 'type': bool, 'arg': '--split-album-discs' },
DOWNLOAD_LYRICS: { 'default': 'True', 'type': bool, 'arg': '--download-lyrics' },
LYRICS_LOCATION: { 'default': '', 'type': str, 'arg': '--lyrics-location' },
LYRICS_FILENAME: { 'default': '{artist}_{song_name}', 'type': str, 'arg': '--lyrics-filename' },
ALWAYS_CHECK_LYRICS: { 'default': 'False', 'type': bool, 'arg': '--always-check-lyrics' },
LYRICS_MD_HEADER: { 'default': 'False', 'type': bool, 'arg': '--lyrics-md-header' },
MD_SAVE_LYRICS: { 'default': 'True', 'type': bool, 'arg': '--md-save-lyrics' },
UNIQUE_LYRICS_FILE: { 'default': 'False', 'type': bool, 'arg': '--unique-file' },
MD_SAVE_GENRES: { 'default': 'False', 'type': bool, 'arg': '--md-save-genres' },
MD_ALLGENRES: { 'default': 'False', 'type': bool, 'arg': '--md-allgenres' },
MD_GENREDELIMITER: { 'default': ',', 'type': str, 'arg': '--md-genredelimiter' },
@@ -68,7 +81,8 @@ CONFIG_VALUES = {
PRINT_API_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-api-errors' },
PRINT_PROGRESS_INFO: { 'default': 'True', 'type': bool, 'arg': '--print-progress-info' },
PRINT_WARNINGS: { 'default': 'True', 'type': bool, 'arg': '--print-warnings' },
TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' }
TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' },
LOCALE: { 'default': 'en-EN', 'type': str, 'arg': '--locale' }
}
OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}'
@@ -116,10 +130,18 @@ class Config:
cls.Values[key] = cls.parse_arg_value(key, CONFIG_VALUES[key]['default'])
# Override config from commandline arguments
# Prefer using the argparse-derived dest name from the configured '--long-option'
for key in CONFIG_VALUES:
if key.lower() in vars(args) and vars(args)[key.lower()] is not None:
cls.Values[key] = cls.parse_arg_value(key, vars(args)[key.lower()])
arg_flag = CONFIG_VALUES[key]['arg']
dest_name = arg_flag.lstrip('-').replace('-', '_') if isinstance(arg_flag, str) else None
args_ns = vars(args)
# 1) Use dest_name if present (e.g., '--unique-file' -> 'unique_file')
if dest_name and dest_name in args_ns and args_ns[dest_name] is not None:
cls.Values[key] = cls.parse_arg_value(key, args_ns[dest_name])
continue
# 2) Fallback to legacy behavior: key.lower() (e.g., 'LYRICS_FILENAME' -> 'lyrics_filename')
if key.lower() in args_ns and args_ns[key.lower()] is not None:
cls.Values[key] = cls.parse_arg_value(key, args_ns[key.lower()])
if args.no_splash:
cls.Values[PRINT_SPLASH] = False
@@ -197,6 +219,37 @@ class Config:
def get_download_lyrics(cls) -> bool:
return cls.get(DOWNLOAD_LYRICS)
@classmethod
def get_lyrics_location(cls):
"""Returns PurePath or None when empty (meaning use track output directory)."""
v = cls.get(LYRICS_LOCATION)
if not v:
return None
p = str(v)
if p.startswith('.'):
return PurePath(cls.get_root_path()).joinpath(PurePath(p).relative_to('.'))
return PurePath(Path(p).expanduser())
@classmethod
def get_lyrics_filename(cls) -> str:
return cls.get(LYRICS_FILENAME)
@classmethod
def get_always_check_lyrics(cls) -> bool:
return cls.get(ALWAYS_CHECK_LYRICS)
@classmethod
def get_lyrics_md_header(cls) -> bool:
return cls.get(LYRICS_MD_HEADER)
@classmethod
def get_save_lyrics_tags(cls) -> bool:
return cls.get(MD_SAVE_LYRICS)
@classmethod
def get_unique_lyrics_file(cls) -> bool:
return cls.get(UNIQUE_LYRICS_FILE)
@classmethod
def get_bulk_wait_time(cls) -> int:
return cls.get(BULK_WAIT_TIME)
@@ -217,6 +270,10 @@ class Config:
def get_transcode_bitrate(cls) -> str:
return cls.get(TRANSCODE_BITRATE)
@classmethod
def get_locale(cls) -> str:
return cls.get(LOCALE)
@classmethod
def get_song_archive(cls) -> str:
if cls.get(SONG_ARCHIVE) == '':

View File

@@ -15,7 +15,8 @@ def get_all_playlists():
offset = 0
while True:
resp = Zotify.invoke_url_with_params(MY_PLAYLISTS_URL, limit=limit, offset=offset)
# Request with locale to ensure playlist names are localized in UI and any downstream usage
resp = Zotify.invoke_url_with_params(MY_PLAYLISTS_URL, limit=limit, offset=offset, market='from_token', locale=Zotify.CONFIG.get_locale())
offset += limit
playlists.extend(resp[ITEMS])
if len(resp[ITEMS]) < limit:
@@ -31,7 +32,14 @@ def get_playlist_songs(playlist_id):
limit = 100
while True:
resp = Zotify.invoke_url_with_params(f'{PLAYLISTS_URL}/{playlist_id}/tracks', limit=limit, offset=offset)
# Include locale so returned track objects have localized fields (cosmetic; filenames use per-track fetch)
resp = Zotify.invoke_url_with_params(
f'{PLAYLISTS_URL}/{playlist_id}/tracks',
limit=limit,
offset=offset,
market='from_token',
locale=Zotify.CONFIG.get_locale()
)
offset += limit
songs.extend(resp[ITEMS])
if len(resp[ITEMS]) < limit:
@@ -48,12 +56,20 @@ def get_playlist_info(playlist_id):
def download_playlist(playlist):
"""Downloads all the songs from a playlist"""
# Fetch localized playlist name using the configured locale so folder names are localized too
pl_name, _ = get_playlist_info(playlist[ID])
playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK] is not None and song[TRACK][ID]]
p_bar = Printer.progress(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True)
enum = 1
for song in p_bar:
download_track('extplaylist', song[TRACK][ID], extra_keys={'playlist': playlist[NAME], 'playlist_num': str(enum).zfill(2)}, disable_progressbar=True)
# Use localized playlist name; track metadata (artist/title) is localized in download_track via locale
download_track(
'extplaylist',
song[TRACK][ID],
extra_keys={'playlist': pl_name, 'playlist_num': str(enum).zfill(2)},
disable_progressbar=True
)
p_bar.set_description(song[TRACK][NAME])
enum += 1