added real time downloading

This commit is contained in:
cool.gitter.choco
2025-01-31 19:45:55 -06:00
parent a10e99e788
commit 3a1315cdbc
11 changed files with 149 additions and 49 deletions

View File

@@ -27,7 +27,7 @@ class FlushingFileWrapper:
def flush(self): def flush(self):
self.file.flush() self.file.flush()
def download_task(service, url, main, fallback, quality, fall_quality, prg_path): def download_task(service, url, main, fallback, quality, fall_quality, real_time, prg_path):
try: try:
from routes.utils.album import download_album from routes.utils.album import download_album
with open(prg_path, 'w') as f: with open(prg_path, 'w') as f:
@@ -42,7 +42,8 @@ def download_task(service, url, main, fallback, quality, fall_quality, prg_path)
main=main, main=main,
fallback=fallback, fallback=fallback,
quality=quality, quality=quality,
fall_quality=fall_quality fall_quality=fall_quality,
real_time=real_time
) )
flushing_file.write(json.dumps({"status": "complete"}) + "\n") flushing_file.write(json.dumps({"status": "complete"}) + "\n")
except Exception as e: except Exception as e:
@@ -71,6 +72,10 @@ def handle_download():
fallback = request.args.get('fallback') fallback = request.args.get('fallback')
quality = request.args.get('quality') quality = request.args.get('quality')
fall_quality = request.args.get('fall_quality') fall_quality = request.args.get('fall_quality')
# Retrieve and normalize the real_time parameter; defaults to False.
real_time_arg = request.args.get('real_time', 'false')
real_time = real_time_arg.lower() in ['true', '1', 'yes']
# Sanitize main and fallback to prevent directory traversal # Sanitize main and fallback to prevent directory traversal
if main: if main:
@@ -142,11 +147,11 @@ def handle_download():
Process( Process(
target=download_task, target=download_task,
args=(service, url, main, fallback, quality, fall_quality, prg_path) args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
).start() ).start()
return Response( return Response(
json.dumps({"prg_file": filename}), json.dumps({"prg_file": filename}),
status=202, status=202,
mimetype='application/json' mimetype='application/json'
) )

View File

@@ -26,7 +26,7 @@ class FlushingFileWrapper:
def flush(self): def flush(self):
self.file.flush() self.file.flush()
def download_task(service, url, main, fallback, quality, fall_quality, prg_path): def download_task(service, url, main, fallback, quality, fall_quality, real_time, prg_path):
try: try:
from routes.utils.playlist import download_playlist from routes.utils.playlist import download_playlist
with open(prg_path, 'w') as f: with open(prg_path, 'w') as f:
@@ -41,7 +41,8 @@ def download_task(service, url, main, fallback, quality, fall_quality, prg_path)
main=main, main=main,
fallback=fallback, fallback=fallback,
quality=quality, quality=quality,
fall_quality=fall_quality fall_quality=fall_quality,
real_time=real_time
) )
flushing_file.write(json.dumps({"status": "complete"}) + "\n") flushing_file.write(json.dumps({"status": "complete"}) + "\n")
except Exception as e: except Exception as e:
@@ -71,6 +72,11 @@ def handle_download():
quality = request.args.get('quality') quality = request.args.get('quality')
fall_quality = request.args.get('fall_quality') fall_quality = request.args.get('fall_quality')
# Retrieve the real_time parameter from the request query string.
# Here, if real_time is provided as "true", "1", or "yes" (case-insensitive) it will be interpreted as True.
real_time_str = request.args.get('real_time', 'false').lower()
real_time = real_time_str in ['true', '1', 'yes']
if not all([service, url, main]): if not all([service, url, main]):
return Response( return Response(
json.dumps({"error": "Missing parameters"}), json.dumps({"error": "Missing parameters"}),
@@ -85,11 +91,11 @@ def handle_download():
Process( Process(
target=download_task, target=download_task,
args=(service, url, main, fallback, quality, fall_quality, prg_path) args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
).start() ).start()
return Response( return Response(
json.dumps({"prg_file": filename}), json.dumps({"prg_file": filename}),
status=202, status=202,
mimetype='application/json' mimetype='application/json'
) )

View File

@@ -18,6 +18,7 @@ class FlushingFileWrapper:
self.file = file self.file = file
def write(self, text): def write(self, text):
# Write only lines that start with a JSON object
for line in text.split('\n'): for line in text.split('\n'):
if line.startswith('{'): if line.startswith('{'):
self.file.write(line + '\n') self.file.write(line + '\n')
@@ -26,13 +27,13 @@ class FlushingFileWrapper:
def flush(self): def flush(self):
self.file.flush() self.file.flush()
def download_task(service, url, main, fallback, quality, fall_quality, prg_path): def download_task(service, url, main, fallback, quality, fall_quality, real_time, prg_path):
try: try:
from routes.utils.track import download_track from routes.utils.track import download_track
with open(prg_path, 'w') as f: with open(prg_path, 'w') as f:
flushing_file = FlushingFileWrapper(f) flushing_file = FlushingFileWrapper(f)
original_stdout = sys.stdout original_stdout = sys.stdout
sys.stdout = flushing_file # Redirect stdout per process sys.stdout = flushing_file # Redirect stdout for this process
try: try:
download_track( download_track(
@@ -41,7 +42,8 @@ def download_task(service, url, main, fallback, quality, fall_quality, prg_path)
main=main, main=main,
fallback=fallback, fallback=fallback,
quality=quality, quality=quality,
fall_quality=fall_quality fall_quality=fall_quality,
real_time=real_time
) )
flushing_file.write(json.dumps({"status": "complete"}) + "\n") flushing_file.write(json.dumps({"status": "complete"}) + "\n")
except Exception as e: except Exception as e:
@@ -71,6 +73,16 @@ def handle_download():
quality = request.args.get('quality') quality = request.args.get('quality')
fall_quality = request.args.get('fall_quality') fall_quality = request.args.get('fall_quality')
# Retrieve and normalize the real_time parameter; defaults to False.
real_time_arg = request.args.get('real_time', 'false')
real_time = real_time_arg.lower() in ['true', '1', 'yes']
# Sanitize main and fallback to prevent directory traversal
if main:
main = os.path.basename(main)
if fallback:
fallback = os.path.basename(fallback)
if not all([service, url, main]): if not all([service, url, main]):
return Response( return Response(
json.dumps({"error": "Missing parameters"}), json.dumps({"error": "Missing parameters"}),
@@ -78,6 +90,56 @@ def handle_download():
mimetype='application/json' mimetype='application/json'
) )
# Validate credentials based on service and fallback
try:
if service == 'spotify':
if fallback:
# Validate Deezer main credentials and Spotify fallback credentials
deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json'))
if not os.path.isfile(deezer_creds_path):
return Response(
json.dumps({"error": "Invalid Deezer credentials directory"}),
status=400,
mimetype='application/json'
)
spotify_fallback_path = os.path.abspath(os.path.join('./creds/spotify', fallback, 'credentials.json'))
if not os.path.isfile(spotify_fallback_path):
return Response(
json.dumps({"error": "Invalid Spotify fallback credentials directory"}),
status=400,
mimetype='application/json'
)
else:
# Validate Spotify main credentials
spotify_creds_path = os.path.abspath(os.path.join('./creds/spotify', main, 'credentials.json'))
if not os.path.isfile(spotify_creds_path):
return Response(
json.dumps({"error": "Invalid Spotify credentials directory"}),
status=400,
mimetype='application/json'
)
elif service == 'deezer':
# Validate Deezer main credentials
deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json'))
if not os.path.isfile(deezer_creds_path):
return Response(
json.dumps({"error": "Invalid Deezer credentials directory"}),
status=400,
mimetype='application/json'
)
else:
return Response(
json.dumps({"error": "Unsupported service"}),
status=400,
mimetype='application/json'
)
except Exception as e:
return Response(
json.dumps({"error": f"Credential validation failed: {str(e)}"}),
status=500,
mimetype='application/json'
)
filename = generate_random_filename() filename = generate_random_filename()
prg_dir = './prgs' prg_dir = './prgs'
os.makedirs(prg_dir, exist_ok=True) os.makedirs(prg_dir, exist_ok=True)
@@ -85,11 +147,11 @@ def handle_download():
Process( Process(
target=download_task, target=download_task,
args=(service, url, main, fallback, quality, fall_quality, prg_path) args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
).start() ).start()
return Response( return Response(
json.dumps({"prg_file": filename}), json.dumps({"prg_file": filename}),
status=202, status=202,
mimetype='application/json' mimetype='application/json'
) )

View File

@@ -4,15 +4,14 @@ import traceback
from deezspot.spotloader import SpoLogin from deezspot.spotloader import SpoLogin
from deezspot.deezloader import DeeLogin from deezspot.deezloader import DeeLogin
def download_album(service, url, main, fallback=None, quality=None, fall_quality=None): def download_album(service, url, main, fallback=None, quality=None, fall_quality=None, real_time=False):
try: try:
if service == 'spotify': if service == 'spotify':
if fallback: if fallback:
if quality is None: if quality is None:
quality = 'FLAC' quality = 'FLAC'
if fall_quality is None: if fall_quality is None:
fall_quality='HIGH' fall_quality = 'HIGH'
# First attempt: use DeeLogin's download_albumspo with the 'main' (Deezer credentials) # First attempt: use DeeLogin's download_albumspo with the 'main' (Deezer credentials)
try: try:
# Load Deezer credentials from 'main' under deezer directory # Load Deezer credentials from 'main' under deezer directory
@@ -24,7 +23,7 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
dl = DeeLogin( dl = DeeLogin(
arl=deezer_creds.get('arl', ''), arl=deezer_creds.get('arl', ''),
) )
# Download using download_albumspo # Download using download_albumspo; pass real_time_dl accordingly
dl.download_albumspo( dl.download_albumspo(
link_album=url, link_album=url,
output_dir="./downloads", output_dir="./downloads",
@@ -33,7 +32,8 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
make_zip=False, make_zip=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
except Exception as e: except Exception as e:
# Load fallback Spotify credentials and attempt download # Load fallback Spotify credentials and attempt download
@@ -49,7 +49,8 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
except Exception as e2: except Exception as e2:
# If fallback also fails, raise an error indicating both attempts failed # If fallback also fails, raise an error indicating both attempts failed
@@ -60,7 +61,7 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
else: else:
# Original behavior: use Spotify main # Original behavior: use Spotify main
if quality is None: if quality is None:
quality ='HIGH' quality = 'HIGH'
creds_dir = os.path.join('./creds/spotify', main) creds_dir = os.path.join('./creds/spotify', main)
credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
spo = SpoLogin(credentials_path=credentials_path) spo = SpoLogin(credentials_path=credentials_path)
@@ -72,11 +73,12 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
elif service == 'deezer': elif service == 'deezer':
if quality is None: if quality is None:
quality='FLAC' quality = 'FLAC'
# Existing code remains the same, ignoring fallback # Existing code remains the same, ignoring fallback
creds_dir = os.path.join('./creds/deezer', main) creds_dir = os.path.join('./creds/deezer', main)
creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
@@ -92,10 +94,11 @@ def download_album(service, url, main, fallback=None, quality=None, fall_quality
recursive_quality=True, recursive_quality=True,
recursive_download=False, recursive_download=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
else: else:
raise ValueError(f"Unsupported service: {service}") raise ValueError(f"Unsupported service: {service}")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise # Re-raise the exception after logging raise # Re-raise the exception after logging

View File

@@ -4,15 +4,14 @@ import traceback
from deezspot.spotloader import SpoLogin from deezspot.spotloader import SpoLogin
from deezspot.deezloader import DeeLogin from deezspot.deezloader import DeeLogin
def download_playlist(service, url, main, fallback=None, quality=None, fall_quality=None): def download_playlist(service, url, main, fallback=None, quality=None, fall_quality=None, real_time=False):
try: try:
if service == 'spotify': if service == 'spotify':
if fallback: if fallback:
if quality is None: if quality is None:
quality = 'FLAC' quality = 'FLAC'
if fall_quality is None: if fall_quality is None:
fall_quality='HIGH' fall_quality = 'HIGH'
# First attempt: use DeeLogin's download_playlistspo with the 'main' (Deezer credentials) # First attempt: use DeeLogin's download_playlistspo with the 'main' (Deezer credentials)
try: try:
# Load Deezer credentials from 'main' under deezer directory # Load Deezer credentials from 'main' under deezer directory
@@ -33,7 +32,8 @@ def download_playlist(service, url, main, fallback=None, quality=None, fall_qual
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
make_zip=False, make_zip=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
except Exception as e: except Exception as e:
# Load fallback Spotify credentials and attempt download # Load fallback Spotify credentials and attempt download
@@ -49,7 +49,8 @@ def download_playlist(service, url, main, fallback=None, quality=None, fall_qual
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
except Exception as e2: except Exception as e2:
# If fallback also fails, raise an error indicating both attempts failed # If fallback also fails, raise an error indicating both attempts failed
@@ -60,7 +61,7 @@ def download_playlist(service, url, main, fallback=None, quality=None, fall_qual
else: else:
# Original behavior: use Spotify main # Original behavior: use Spotify main
if quality is None: if quality is None:
quality='HIGH' quality = 'HIGH'
creds_dir = os.path.join('./creds/spotify', main) creds_dir = os.path.join('./creds/spotify', main)
credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
spo = SpoLogin(credentials_path=credentials_path) spo = SpoLogin(credentials_path=credentials_path)
@@ -72,11 +73,12 @@ def download_playlist(service, url, main, fallback=None, quality=None, fall_qual
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
elif service == 'deezer': elif service == 'deezer':
if quality is None: if quality is None:
quality='FLAC' quality = 'FLAC'
# Existing code for Deezer, using main as Deezer account # Existing code for Deezer, using main as Deezer account
creds_dir = os.path.join('./creds/deezer', main) creds_dir = os.path.join('./creds/deezer', main)
creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
@@ -92,10 +94,11 @@ def download_playlist(service, url, main, fallback=None, quality=None, fall_qual
recursive_quality=False, recursive_quality=False,
recursive_download=False, recursive_download=False,
method_save=1, method_save=1,
make_zip=False make_zip=False,
real_time_dl=real_time
) )
else: else:
raise ValueError(f"Unsupported service: {service}") raise ValueError(f"Unsupported service: {service}")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise # Re-raise the exception after logging raise # Re-raise the exception after logging

View File

@@ -4,15 +4,14 @@ import traceback
from deezspot.spotloader import SpoLogin from deezspot.spotloader import SpoLogin
from deezspot.deezloader import DeeLogin from deezspot.deezloader import DeeLogin
def download_track(service, url, main, fallback=None, quality=None, fall_quality=None): def download_track(service, url, main, fallback=None, quality=None, fall_quality=None, real_time=False):
try: try:
if service == 'spotify': if service == 'spotify':
if fallback: if fallback:
if quality is None: if quality is None:
quality = 'FLAC' quality = 'FLAC'
if fall_quality is None: if fall_quality is None:
fall_quality='HIGH' fall_quality = 'HIGH'
# First attempt: use Deezer's download_trackspo with 'main' (Deezer credentials) # First attempt: use Deezer's download_trackspo with 'main' (Deezer credentials)
try: try:
deezer_creds_dir = os.path.join('./creds/deezer', main) deezer_creds_dir = os.path.join('./creds/deezer', main)
@@ -29,7 +28,8 @@ def download_track(service, url, main, fallback=None, quality=None, fall_quality
recursive_quality=False, recursive_quality=False,
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
except Exception as e: except Exception as e:
spo_creds_dir = os.path.join('./creds/spotify', fallback) spo_creds_dir = os.path.join('./creds/spotify', fallback)
@@ -42,12 +42,13 @@ def download_track(service, url, main, fallback=None, quality=None, fall_quality
recursive_quality=False, recursive_quality=False,
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
else: else:
# Directly use Spotify main account # Directly use Spotify main account
if quality is None: if quality is None:
quality='HIGH' quality = 'HIGH'
creds_dir = os.path.join('./creds/spotify', main) creds_dir = os.path.join('./creds/spotify', main)
credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
spo = SpoLogin(credentials_path=credentials_path) spo = SpoLogin(credentials_path=credentials_path)
@@ -58,12 +59,13 @@ def download_track(service, url, main, fallback=None, quality=None, fall_quality
recursive_quality=False, recursive_quality=False,
recursive_download=False, recursive_download=False,
not_interface=False, not_interface=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
elif service == 'deezer': elif service == 'deezer':
if quality is None: if quality is None:
quality='FLAC' quality = 'FLAC'
# Deezer download logic remains unchanged # Deezer download logic remains unchanged, with real_time_dl passed accordingly
creds_dir = os.path.join('./creds/deezer', main) creds_dir = os.path.join('./creds/deezer', main)
creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json'))
with open(creds_path, 'r') as f: with open(creds_path, 'r') as f:
@@ -77,10 +79,11 @@ def download_track(service, url, main, fallback=None, quality=None, fall_quality
quality_download=quality, quality_download=quality,
recursive_quality=False, recursive_quality=False,
recursive_download=False, recursive_download=False,
method_save=1 method_save=1,
real_time_dl=real_time
) )
else: else:
raise ValueError(f"Unsupported service: {service}") raise ValueError(f"Unsupported service: {service}")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise raise

View File

@@ -319,6 +319,12 @@ async function startDownload(url, type, item) {
apiUrl += `&quality=${encodeURIComponent(service === 'spotify' ? spotifyQuality : deezerQuality)}`; apiUrl += `&quality=${encodeURIComponent(service === 'spotify' ? spotifyQuality : deezerQuality)}`;
} }
// New: append real_time parameter if Real time downloading is enabled
const realTimeEnabled = document.getElementById('realTimeToggle').checked;
if (realTimeEnabled) {
apiUrl += `&real_time=true`;
}
try { try {
const response = await fetch(apiUrl); const response = await fetch(apiUrl);
const data = await response.json(); const data = await response.json();
@@ -326,7 +332,8 @@ async function startDownload(url, type, item) {
} catch (error) { } catch (error) {
showError('Download failed: ' + error.message); showError('Download failed: ' + error.message);
} }
} }
function addToQueue(item, type, prgFile) { function addToQueue(item, type, prgFile) {
const queueId = Date.now().toString() + Math.random().toString(36).substr(2, 9); const queueId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
@@ -673,12 +680,12 @@ function saveConfig() {
deezer: document.getElementById('deezerAccountSelect').value, deezer: document.getElementById('deezerAccountSelect').value,
fallback: document.getElementById('fallbackToggle').checked, fallback: document.getElementById('fallbackToggle').checked,
spotifyQuality: document.getElementById('spotifyQualitySelect').value, spotifyQuality: document.getElementById('spotifyQualitySelect').value,
deezerQuality: document.getElementById('deezerQualitySelect').value deezerQuality: document.getElementById('deezerQualitySelect').value,
realTime: document.getElementById('realTimeToggle').checked // new property
}; };
localStorage.setItem('activeConfig', JSON.stringify(config)); localStorage.setItem('activeConfig', JSON.stringify(config));
} }
function loadConfig() { function loadConfig() {
const saved = JSON.parse(localStorage.getItem('activeConfig')) || {}; const saved = JSON.parse(localStorage.getItem('activeConfig')) || {};
@@ -699,7 +706,11 @@ function loadConfig() {
const deezerQuality = document.getElementById('deezerQualitySelect'); const deezerQuality = document.getElementById('deezerQualitySelect');
if (deezerQuality) deezerQuality.value = saved.deezerQuality || 'MP3_128'; if (deezerQuality) deezerQuality.value = saved.deezerQuality || 'MP3_128';
}
// New: Real time downloading toggle
const realTimeToggle = document.getElementById('realTimeToggle');
if (realTimeToggle) realTimeToggle.checked = !!saved.realTime;
}
function isSpotifyUrl(url) { function isSpotifyUrl(url) {
return url.startsWith('https://open.spotify.com/'); return url.startsWith('https://open.spotify.com/');

View File

@@ -46,6 +46,13 @@
<span class="slider"></span> <span class="slider"></span>
</label> </label>
</div> </div>
<div class="config-item">
<label>Real time downloading:</label>
<label class="switch">
<input type="checkbox" id="realTimeToggle">
<span class="slider"></span>
</label>
</div>
</div> </div>
<!-- Service Tabs --> <!-- Service Tabs -->