mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2024-11-26 09:53:17 +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
|
||||
|
||||
from app import client
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user