feat: added real time multiplier setting to frontend
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
fastapi==0.116.1
|
||||
uvicorn[standard]==0.35.0
|
||||
celery==5.5.3
|
||||
deezspot-spotizerr==2.6.0
|
||||
deezspot-spotizerr==2.7.1
|
||||
httpx==0.28.1
|
||||
bcrypt==4.2.1
|
||||
PyJWT==2.10.1
|
||||
|
||||
@@ -30,6 +30,9 @@ from routes.utils.get_info import (
|
||||
) # To fetch playlist, track, artist, and album details
|
||||
from routes.utils.celery_queue_manager import download_queue_manager
|
||||
|
||||
# Added import to fetch base formatting config
|
||||
from routes.utils.celery_queue_manager import get_config_params
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAIN_CONFIG_FILE_PATH = Path("./data/config/main.json")
|
||||
WATCH_OLD_FILE_PATH = Path("./data/config/watch.json")
|
||||
@@ -153,6 +156,38 @@ def construct_spotify_url(item_id, item_type="track"):
|
||||
return f"https://open.spotify.com/{item_type}/{item_id}"
|
||||
|
||||
|
||||
# Helper to replace playlist placeholders in custom formats per-track
|
||||
def _apply_playlist_placeholders(
|
||||
base_dir_fmt: str,
|
||||
base_track_fmt: str,
|
||||
playlist_name: str,
|
||||
playlist_position_one_based: int,
|
||||
total_tracks_in_playlist: int,
|
||||
pad_tracks: bool,
|
||||
) -> tuple[str, str]:
|
||||
try:
|
||||
width = max(2, len(str(total_tracks_in_playlist))) if pad_tracks else 0
|
||||
if (
|
||||
pad_tracks
|
||||
and playlist_position_one_based is not None
|
||||
and playlist_position_one_based > 0
|
||||
):
|
||||
playlist_num_str = str(playlist_position_one_based).zfill(width)
|
||||
else:
|
||||
playlist_num_str = (
|
||||
str(playlist_position_one_based) if playlist_position_one_based else ""
|
||||
)
|
||||
|
||||
dir_fmt = base_dir_fmt.replace("%playlist%", playlist_name)
|
||||
track_fmt = base_track_fmt.replace("%playlist%", playlist_name).replace(
|
||||
"%playlistnum%", playlist_num_str
|
||||
)
|
||||
return dir_fmt, track_fmt
|
||||
except Exception:
|
||||
# On any error, return originals
|
||||
return base_dir_fmt, base_track_fmt
|
||||
|
||||
|
||||
def has_playlist_changed(playlist_spotify_id: str, current_snapshot_id: str) -> bool:
|
||||
"""
|
||||
Check if a playlist has changed by comparing snapshot_id.
|
||||
@@ -320,6 +355,11 @@ def check_watched_playlists(specific_playlist_id: str = None):
|
||||
)
|
||||
config = get_watch_config()
|
||||
use_snapshot_checking = config.get("useSnapshotIdChecking", True)
|
||||
# Fetch base formatting configuration once for this run
|
||||
formatting_cfg = get_config_params()
|
||||
base_dir_fmt = formatting_cfg.get("customDirFormat", "%ar_album%/%album%")
|
||||
base_track_fmt = formatting_cfg.get("customTrackFormat", "%tracknum%. %music%")
|
||||
pad_tracks = formatting_cfg.get("tracknumPadding", True)
|
||||
|
||||
if specific_playlist_id:
|
||||
playlist_obj = get_watched_playlist(specific_playlist_id)
|
||||
@@ -483,12 +523,17 @@ def check_watched_playlists(specific_playlist_id: str = None):
|
||||
|
||||
current_api_track_ids = set()
|
||||
api_track_id_to_item_map = {}
|
||||
for item in all_api_track_items: # Use all_api_track_items
|
||||
api_track_position_map: dict[str, int] = {}
|
||||
# Build maps for quick lookup and position within the playlist (1-based)
|
||||
for idx, item in enumerate(
|
||||
all_api_track_items, start=1
|
||||
): # Use overall playlist index for numbering
|
||||
track = item.get("track")
|
||||
if track and track.get("id") and not track.get("is_local"):
|
||||
track_id = track["id"]
|
||||
current_api_track_ids.add(track_id)
|
||||
api_track_id_to_item_map[track_id] = item
|
||||
api_track_position_map[track_id] = idx
|
||||
|
||||
db_track_ids = get_playlist_track_ids_from_db(playlist_spotify_id)
|
||||
|
||||
@@ -507,6 +552,19 @@ def check_watched_playlists(specific_playlist_id: str = None):
|
||||
continue
|
||||
|
||||
track_to_queue = api_item["track"]
|
||||
# Compute per-track formatting overrides for playlist placeholders
|
||||
position_in_playlist = api_track_position_map.get(track_id)
|
||||
custom_dir_format, custom_track_format = (
|
||||
_apply_playlist_placeholders(
|
||||
base_dir_fmt,
|
||||
base_track_fmt,
|
||||
playlist_name,
|
||||
position_in_playlist if position_in_playlist else 0,
|
||||
api_total_tracks,
|
||||
pad_tracks,
|
||||
)
|
||||
)
|
||||
|
||||
task_payload = {
|
||||
"download_type": "track",
|
||||
"url": construct_spotify_url(track_id, "track"),
|
||||
@@ -525,7 +583,9 @@ def check_watched_playlists(specific_playlist_id: str = None):
|
||||
"track_spotify_id": track_id,
|
||||
"track_item_for_db": api_item, # Pass full API item for DB update on completion
|
||||
},
|
||||
# "track_details_for_db" was old name, using track_item_for_db consistent with celery_tasks
|
||||
# Override formats so %playlist% and %playlistnum% resolve now per track
|
||||
"custom_dir_format": custom_dir_format,
|
||||
"custom_track_format": custom_track_format,
|
||||
}
|
||||
try:
|
||||
task_id_or_none = download_queue_manager.add_task(
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DownloadSettings {
|
||||
spotifyQuality: "NORMAL" | "HIGH" | "VERY_HIGH";
|
||||
recursiveQuality: boolean; // frontend field (sent as camelCase to backend)
|
||||
separateTracksByUser: boolean;
|
||||
realTimeMultiplier: number;
|
||||
}
|
||||
|
||||
interface WatchConfig {
|
||||
@@ -150,7 +151,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
const missingServices: string[] = [];
|
||||
if (!spotifyCredentials?.length) missingServices.push("Spotify");
|
||||
if (!deezerCredentials?.length) missingServices.push("Deezer");
|
||||
const error = `Download Fallback requires accounts to be configured for both services. Missing: ${missingServices.join(", ")}. Configure accounts in the Accounts tab.`;
|
||||
const error = `Download Fallback requires accounts to be configured for both Spotify and Deezer. Missing: ${missingServices.join(", ")}. Configure accounts in the Accounts tab.`;
|
||||
setValidationError(error);
|
||||
toast.error("Validation failed: " + error);
|
||||
return;
|
||||
@@ -162,6 +163,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
maxRetries: Number(data.maxRetries),
|
||||
retryDelaySeconds: Number(data.retryDelaySeconds),
|
||||
retryDelayIncrease: Number(data.retryDelayIncrease),
|
||||
realTimeMultiplier: Number(data.realTimeMultiplier ?? 0),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -188,6 +190,26 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
<label htmlFor="realTimeToggle" className="text-content-primary dark:text-content-primary-dark">Real-time downloading</label>
|
||||
<input id="realTimeToggle" type="checkbox" {...register("realTime")} className="h-6 w-6 rounded" />
|
||||
</div>
|
||||
{/* Real-time Multiplier (Spotify only) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="realTimeMultiplier" className="text-content-primary dark:text-content-primary-dark">Real-time speed multiplier (Spotify)</label>
|
||||
<span className="text-xs text-content-secondary dark:text-content-secondary-dark">0–10</span>
|
||||
</div>
|
||||
<input
|
||||
id="realTimeMultiplier"
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
{...register("realTimeMultiplier")}
|
||||
disabled={!realTime}
|
||||
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus disabled:opacity-50"
|
||||
/>
|
||||
<p className="text-xs text-content-muted dark:text-content-muted-dark">
|
||||
Controls how fast Spotify real-time downloads go. Only affects Spotify downloads; ignored for Deezer.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="fallbackToggle" className="text-content-primary dark:text-content-primary-dark">Download Fallback</label>
|
||||
<input id="fallbackToggle" type="checkbox" {...register("fallback")} className="h-6 w-6 rounded" />
|
||||
|
||||
@@ -62,6 +62,7 @@ export type FlatAppSettings = {
|
||||
compilation: string;
|
||||
artistSeparator: string;
|
||||
spotifyMetadata: boolean;
|
||||
realTimeMultiplier: number;
|
||||
};
|
||||
|
||||
const defaultSettings: FlatAppSettings = {
|
||||
@@ -102,6 +103,7 @@ const defaultSettings: FlatAppSettings = {
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
realTimeMultiplier: 0,
|
||||
};
|
||||
|
||||
interface FetchedCamelCaseSettings {
|
||||
@@ -129,6 +131,7 @@ const fetchSettings = async (): Promise<FlatAppSettings> => {
|
||||
...(camelData as unknown as FlatAppSettings),
|
||||
// Ensure required frontend-only fields exist
|
||||
recursiveQuality: Boolean((camelData as any).recursiveQuality ?? false),
|
||||
realTimeMultiplier: Number((camelData as any).realTimeMultiplier ?? 0),
|
||||
};
|
||||
|
||||
return withDefaults;
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface AppSettings {
|
||||
// Add other watch properties from the old type if they still exist in the API response
|
||||
};
|
||||
// Add other root-level properties from the API if they exist
|
||||
realTimeMultiplier: number;
|
||||
}
|
||||
|
||||
export interface SettingsContextType {
|
||||
|
||||
Reference in New Issue
Block a user