mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2024-11-29 19:24:34 +01:00
realtime downloading for podcasts
This commit is contained in:
parent
2f9766859a
commit
dc20e4367c
@ -1,3 +1,23 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
ZSpotify
|
||||||
|
It's like youtube-dl, but for Spotify.
|
||||||
|
Copyright (C) 2021 Deathmonger/Footsiefat and ZSpotify Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from app import client
|
from app import client
|
||||||
|
@ -19,6 +19,7 @@ def client(args) -> None:
|
|||||||
""" Connects to spotify to perform query's and get songs to download """
|
""" Connects to spotify to perform query's and get songs to download """
|
||||||
ZSpotify(args)
|
ZSpotify(args)
|
||||||
|
|
||||||
|
if ZSpotify.CONFIG.get_print_splash():
|
||||||
Printer.print(PrintChannel.SPLASH, splash())
|
Printer.print(PrintChannel.SPLASH, splash())
|
||||||
|
|
||||||
if ZSpotify.check_premium():
|
if ZSpotify.check_premium():
|
||||||
|
@ -31,12 +31,14 @@ MD_ALLGENRES = 'MD_ALLGENRES'
|
|||||||
MD_GENREDELIMITER = 'MD_GENREDELIMITER'
|
MD_GENREDELIMITER = 'MD_GENREDELIMITER'
|
||||||
PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO'
|
PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO'
|
||||||
PRINT_WARNINGS = 'PRINT_WARNINGS'
|
PRINT_WARNINGS = 'PRINT_WARNINGS'
|
||||||
|
RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
|
||||||
|
|
||||||
CONFIG_VALUES = {
|
CONFIG_VALUES = {
|
||||||
ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' },
|
ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' },
|
||||||
ROOT_PODCAST_PATH: { 'default': '../ZSpotify Podcasts/', 'type': str, 'arg': '--root-podcast-path' },
|
ROOT_PODCAST_PATH: { 'default': '../ZSpotify Podcasts/', 'type': str, 'arg': '--root-podcast-path' },
|
||||||
SKIP_EXISTING_FILES: { 'default': 'True', 'type': bool, 'arg': '--skip-existing-files' },
|
SKIP_EXISTING_FILES: { 'default': 'True', 'type': bool, 'arg': '--skip-existing-files' },
|
||||||
SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' },
|
SKIP_PREVIOUSLY_DOWNLOADED: { 'default': 'False', 'type': bool, 'arg': '--skip-previously-downloaded' },
|
||||||
|
RETRY_ATTEMPTS: { 'default': '5', 'type': int, 'arg': '--retry-attemps' },
|
||||||
DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' },
|
DOWNLOAD_FORMAT: { 'default': 'ogg', 'type': str, 'arg': '--download-format' },
|
||||||
FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' },
|
FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' },
|
||||||
ANTI_BAN_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--anti-ban-wait-time' },
|
ANTI_BAN_WAIT_TIME: { 'default': '1', 'type': int, 'arg': '--anti-ban-wait-time' },
|
||||||
@ -49,7 +51,7 @@ CONFIG_VALUES = {
|
|||||||
SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' },
|
SONG_ARCHIVE: { 'default': '.song_archive', 'type': str, 'arg': '--song-archive' },
|
||||||
CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' },
|
CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' },
|
||||||
OUTPUT: { 'default': '', 'type': str, 'arg': '--output' },
|
OUTPUT: { 'default': '', 'type': str, 'arg': '--output' },
|
||||||
PRINT_SPLASH: { 'default': 'True', 'type': bool, 'arg': '--print-splash' },
|
PRINT_SPLASH: { 'default': 'False', 'type': bool, 'arg': '--print-splash' },
|
||||||
PRINT_SKIPS: { 'default': 'True', 'type': bool, 'arg': '--print-skips' },
|
PRINT_SKIPS: { 'default': 'True', 'type': bool, 'arg': '--print-skips' },
|
||||||
PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' },
|
PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' },
|
||||||
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
|
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
|
||||||
@ -207,6 +209,10 @@ class Config:
|
|||||||
def get_allGenres(cls) -> bool:
|
def get_allGenres(cls) -> bool:
|
||||||
return cls.get(MD_ALLGENRES)
|
return cls.get(MD_ALLGENRES)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_print_splash(cls) -> bool:
|
||||||
|
return cls.get(PRINT_SPLASH)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_allGenresDelimiter(cls) -> bool:
|
def get_allGenresDelimiter(cls) -> bool:
|
||||||
return cls.get(MD_GENREDELIMITER)
|
return cls.get(MD_GENREDELIMITER)
|
||||||
|
@ -1,22 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from librespot.metadata import EpisodeId
|
from librespot.metadata import EpisodeId
|
||||||
|
|
||||||
from const import (ERROR, ID, ITEMS, NAME, SHOW)
|
from const import ERROR, ID, ITEMS, NAME, SHOW, DURATION_MS
|
||||||
from termoutput import PrintChannel, Printer
|
from termoutput import PrintChannel, Printer
|
||||||
from utils import create_download_directory, fix_filename
|
from utils import create_download_directory, fix_filename
|
||||||
from zspotify import ZSpotify
|
from zspotify import ZSpotify
|
||||||
|
from loader import Loader
|
||||||
|
|
||||||
|
|
||||||
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
|
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
|
||||||
SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
||||||
|
|
||||||
|
|
||||||
def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
|
def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
with Loader(PrintChannel.PROGRESS_INFO, "Fetching episode information..."):
|
||||||
(raw, info) = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
|
(raw, info) = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
|
||||||
|
if not info:
|
||||||
|
Printer.print(PrintChannel.ERRORS, "### INVALID EPISODE ID ###")
|
||||||
|
duration_ms = info[DURATION_MS]
|
||||||
if ERROR in info:
|
if ERROR in info:
|
||||||
return None, None
|
return None, None
|
||||||
return fix_filename(info[SHOW][NAME]), fix_filename(info[NAME])
|
return fix_filename(info[SHOW][NAME]), duration_ms, fix_filename(info[NAME])
|
||||||
|
|
||||||
|
|
||||||
def get_show_episodes(show_id_str) -> list:
|
def get_show_episodes(show_id_str) -> list:
|
||||||
@ -24,6 +31,7 @@ def get_show_episodes(show_id_str) -> list:
|
|||||||
offset = 0
|
offset = 0
|
||||||
limit = 50
|
limit = 50
|
||||||
|
|
||||||
|
with Loader(PrintChannel.PROGRESS_INFO, "Fetching episodes..."):
|
||||||
while True:
|
while True:
|
||||||
resp = ZSpotify.invoke_url_with_params(
|
resp = ZSpotify.invoke_url_with_params(
|
||||||
f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset)
|
f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset)
|
||||||
@ -64,12 +72,14 @@ def download_podcast_directly(url, filename):
|
|||||||
|
|
||||||
|
|
||||||
def download_episode(episode_id) -> None:
|
def download_episode(episode_id) -> None:
|
||||||
podcast_name, episode_name = get_episode_info(episode_id)
|
podcast_name, duration_ms, episode_name = get_episode_info(episode_id)
|
||||||
|
|
||||||
extra_paths = podcast_name + '/'
|
extra_paths = podcast_name + '/'
|
||||||
|
prepare_download_loader = Loader(PrintChannel.PROGRESS_INFO, "Preparing download...")
|
||||||
|
prepare_download_loader.start()
|
||||||
|
|
||||||
if podcast_name is None:
|
if podcast_name is None:
|
||||||
Printer.print(PrintChannel.SKIPS, '### SKIPPING: (EPISODE NOT FOUND) ###')
|
Printer.print(PrintChannel.SKIPS, '### SKIPPING: (EPISODE NOT FOUND) ###')
|
||||||
|
prepare_download_loader.stop()
|
||||||
else:
|
else:
|
||||||
filename = podcast_name + ' - ' + episode_name
|
filename = podcast_name + ' - ' + episode_name
|
||||||
|
|
||||||
@ -94,21 +104,31 @@ def download_episode(episode_id) -> None:
|
|||||||
and ZSpotify.CONFIG.get_skip_existing_files()
|
and ZSpotify.CONFIG.get_skip_existing_files()
|
||||||
):
|
):
|
||||||
Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
|
Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
|
||||||
|
prepare_download_loader.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prepare_download_loader.stop()
|
||||||
|
time_start = time.time()
|
||||||
|
downloaded = 0
|
||||||
with open(filepath, 'wb') as file, Printer.progress(
|
with open(filepath, 'wb') as file, Printer.progress(
|
||||||
desc=filename,
|
desc=filename,
|
||||||
total=total_size,
|
total=total_size,
|
||||||
unit='B',
|
unit='B',
|
||||||
unit_scale=True,
|
unit_scale=True,
|
||||||
unit_divisor=1024
|
unit_divisor=1024
|
||||||
) as bar:
|
) as p_bar:
|
||||||
|
prepare_download_loader.stop()
|
||||||
for _ in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
for _ in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
||||||
bar.update(file.write(
|
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
||||||
stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())))
|
p_bar.update(file.write(data))
|
||||||
|
downloaded += len(data)
|
||||||
|
if ZSpotify.CONFIG.get_download_real_time():
|
||||||
|
delta_real = time.time() - time_start
|
||||||
|
delta_want = (downloaded / total_size) * (duration_ms/1000)
|
||||||
|
if delta_want > delta_real:
|
||||||
|
time.sleep(delta_want - delta_real)
|
||||||
else:
|
else:
|
||||||
filepath = os.path.join(download_directory, f"{filename}.mp3")
|
filepath = os.path.join(download_directory, f"{filename}.mp3")
|
||||||
download_podcast_directly(direct_download_url, filepath)
|
download_podcast_directly(direct_download_url, filepath)
|
||||||
|
|
||||||
# convert_audio_format(ROOT_PODCAST_PATH +
|
prepare_download_loader.stop()
|
||||||
# extra_paths + filename + '.ogg')
|
|
||||||
|
@ -15,9 +15,9 @@ from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_down
|
|||||||
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive, fmt_seconds
|
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive, fmt_seconds
|
||||||
from zspotify import ZSpotify
|
from zspotify import ZSpotify
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from loader import Loader
|
from loader import Loader
|
||||||
|
|
||||||
|
|
||||||
def get_saved_tracks() -> list:
|
def get_saved_tracks() -> list:
|
||||||
""" Returns user's saved tracks """
|
""" Returns user's saved tracks """
|
||||||
songs = []
|
songs = []
|
||||||
@ -189,7 +189,7 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
|
|||||||
unit_divisor=1024,
|
unit_divisor=1024,
|
||||||
disable=disable_progressbar
|
disable=disable_progressbar
|
||||||
) as p_bar:
|
) as p_bar:
|
||||||
for chunk in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
for _ in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
||||||
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
||||||
p_bar.update(file.write(data))
|
p_bar.update(file.write(data))
|
||||||
downloaded += len(data)
|
downloaded += len(data)
|
||||||
|
@ -280,3 +280,5 @@ def fmt_seconds(secs: float) -> str:
|
|||||||
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||||
else:
|
else:
|
||||||
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,3 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
|
|
||||||
"""
|
|
||||||
ZSpotify
|
|
||||||
It's like youtube-dl, but for Spotify.
|
|
||||||
|
|
||||||
(Made by Deathmonger/Footsiefat - @doomslayer117:matrix.org)
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
@ -81,7 +73,7 @@ class ZSpotify:
|
|||||||
return requests.get(url, headers=headers, params=params).json()
|
return requests.get(url, headers=headers, params=params).json()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def invoke_url(cls, url, tryCount = 0):
|
def invoke_url(cls, url, tryCount=0):
|
||||||
# we need to import that here, otherwise we will get circular imports!
|
# we need to import that here, otherwise we will get circular imports!
|
||||||
from termoutput import Printer, PrintChannel
|
from termoutput import Printer, PrintChannel
|
||||||
headers = cls.get_auth_header()
|
headers = cls.get_auth_header()
|
||||||
@ -90,8 +82,8 @@ class ZSpotify:
|
|||||||
responsejson = response.json()
|
responsejson = response.json()
|
||||||
|
|
||||||
if 'error' in responsejson:
|
if 'error' in responsejson:
|
||||||
if tryCount < 5:
|
if tryCount < (cls.CONFIG.retry_attemps - 1):
|
||||||
Printer.print(PrintChannel.WARNINGS, f"Spotify API Error (try {tryCount}) ({responsejson['error']['status']}): {responsejson['error']['message']}")
|
Printer.print(PrintChannel.WARNINGS, f"Spotify API Error (try {tryCount + 1}) ({responsejson['error']['status']}): {responsejson['error']['message']}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
return cls.invoke_url(url, tryCount + 1)
|
return cls.invoke_url(url, tryCount + 1)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user