realtime downloading for podcasts

This commit is contained in:
logykk 2021-12-18 22:11:43 +13:00
parent 2f9766859a
commit dc20e4367c
7 changed files with 76 additions and 35 deletions

View File

@ -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
from app import client

View File

@ -19,7 +19,8 @@ def client(args) -> None:
""" Connects to spotify to perform query's and get songs to download """
ZSpotify(args)
Printer.print(PrintChannel.SPLASH, splash())
if ZSpotify.CONFIG.get_print_splash():
Printer.print(PrintChannel.SPLASH, splash())
if ZSpotify.check_premium():
Printer.print(PrintChannel.SPLASH, '[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n')

View File

@ -31,12 +31,14 @@ MD_ALLGENRES = 'MD_ALLGENRES'
MD_GENREDELIMITER = 'MD_GENREDELIMITER'
PRINT_PROGRESS_INFO = 'PRINT_PROGRESS_INFO'
PRINT_WARNINGS = 'PRINT_WARNINGS'
RETRY_ATTEMPTS = 'RETRY_ATTEMPTS'
CONFIG_VALUES = {
ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-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_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' },
FORCE_PREMIUM: { 'default': 'False', 'type': bool, 'arg': '--force-premium' },
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' },
CREDENTIALS_LOCATION: { 'default': 'credentials.json', 'type': str, 'arg': '--credentials-location' },
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_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' },
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
@ -207,6 +209,10 @@ class Config:
def get_allGenres(cls) -> bool:
return cls.get(MD_ALLGENRES)
@classmethod
def get_print_splash(cls) -> bool:
return cls.get(PRINT_SPLASH)
@classmethod
def get_allGenresDelimiter(cls) -> bool:
return cls.get(MD_GENREDELIMITER)

View File

@ -1,22 +1,29 @@
import os
import time
from typing import Optional, Tuple
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 utils import create_download_directory, fix_filename
from zspotify import ZSpotify
from loader import Loader
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
SHOWS_URL = 'https://api.spotify.com/v1/shows'
def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
(raw, info) = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
with Loader(PrintChannel.PROGRESS_INFO, "Fetching episode information..."):
(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:
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:
@ -24,14 +31,15 @@ def get_show_episodes(show_id_str) -> list:
offset = 0
limit = 50
while True:
resp = ZSpotify.invoke_url_with_params(
f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset)
offset += limit
for episode in resp[ITEMS]:
episodes.append(episode[ID])
if len(resp[ITEMS]) < limit:
break
with Loader(PrintChannel.PROGRESS_INFO, "Fetching episodes..."):
while True:
resp = ZSpotify.invoke_url_with_params(
f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset)
offset += limit
for episode in resp[ITEMS]:
episodes.append(episode[ID])
if len(resp[ITEMS]) < limit:
break
return episodes
@ -64,12 +72,14 @@ def download_podcast_directly(url, filename):
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 + '/'
prepare_download_loader = Loader(PrintChannel.PROGRESS_INFO, "Preparing download...")
prepare_download_loader.start()
if podcast_name is None:
Printer.print(PrintChannel.SKIPS, '### SKIPPING: (EPISODE NOT FOUND) ###')
prepare_download_loader.stop()
else:
filename = podcast_name + ' - ' + episode_name
@ -94,21 +104,31 @@ def download_episode(episode_id) -> None:
and ZSpotify.CONFIG.get_skip_existing_files()
):
Printer.print(PrintChannel.SKIPS, "\n### SKIPPING: " + podcast_name + " - " + episode_name + " (EPISODE ALREADY EXISTS) ###")
prepare_download_loader.stop()
return
prepare_download_loader.stop()
time_start = time.time()
downloaded = 0
with open(filepath, 'wb') as file, Printer.progress(
desc=filename,
total=total_size,
unit='B',
unit_scale=True,
unit_divisor=1024
) as bar:
) as p_bar:
prepare_download_loader.stop()
for _ in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
bar.update(file.write(
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))
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:
filepath = os.path.join(download_directory, f"{filename}.mp3")
download_podcast_directly(direct_download_url, filepath)
# convert_audio_format(ROOT_PODCAST_PATH +
# extra_paths + filename + '.ogg')
prepare_download_loader.stop()

View File

@ -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
from zspotify import ZSpotify
import traceback
from loader import Loader
def get_saved_tracks() -> list:
""" Returns user's saved tracks """
songs = []
@ -189,7 +189,7 @@ def download_track(mode: str, track_id: str, extra_keys=None, disable_progressba
unit_divisor=1024,
disable=disable_progressbar
) 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())
p_bar.update(file.write(data))
downloaded += len(data)

View File

@ -280,3 +280,5 @@ def fmt_seconds(secs: float) -> str:
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
else:
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)

View File

@ -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.path
from getpass import getpass
@ -81,17 +73,17 @@ class ZSpotify:
return requests.get(url, headers=headers, params=params).json()
@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!
from termoutput import Printer, PrintChannel
from termoutput import Printer, PrintChannel
headers = cls.get_auth_header()
response = requests.get(url, headers=headers)
responsetext = response.text
responsejson = response.json()
if 'error' in responsejson:
if tryCount < 5:
Printer.print(PrintChannel.WARNINGS, f"Spotify API Error (try {tryCount}) ({responsejson['error']['status']}): {responsejson['error']['message']}")
if tryCount < (cls.CONFIG.retry_attemps - 1):
Printer.print(PrintChannel.WARNINGS, f"Spotify API Error (try {tryCount + 1}) ({responsejson['error']['status']}): {responsejson['error']['message']}")
time.sleep(5)
return cls.invoke_url(url, tryCount + 1)