polishing the edges
This commit is contained in:
@@ -6,24 +6,10 @@
|
||||
/Test.py
|
||||
/prgs/
|
||||
/flask_server.log
|
||||
/routes/__pycache__/
|
||||
routes/utils/__pycache__/
|
||||
test.sh
|
||||
__pycache__/
|
||||
routes/__pycache__/__init__.cpython-312.pyc
|
||||
routes/__pycache__/credentials.cpython-312.pyc
|
||||
routes/__pycache__/search.cpython-312.pyc
|
||||
routes/utils/__pycache__/__init__.cpython-312.pyc
|
||||
routes/utils/__pycache__/credentials.cpython-312.pyc
|
||||
routes/utils/__pycache__/search.cpython-312.pyc
|
||||
routes/utils/__pycache__/__init__.cpython-312.pyc
|
||||
routes/utils/__pycache__/credentials.cpython-312.pyc
|
||||
routes/utils/__pycache__/search.cpython-312.pyc
|
||||
routes/utils/__pycache__/credentials.cpython-312.pyc
|
||||
routes/utils/__pycache__/search.cpython-312.pyc
|
||||
routes/utils/__pycache__/__init__.cpython-312.pyc
|
||||
routes/utils/__pycache__/credentials.cpython-312.pyc
|
||||
routes/utils/__pycache__/search.cpython-312.pyc
|
||||
routes/__pycache__/*
|
||||
routes/utils/__pycache__/*
|
||||
search_test.py
|
||||
config/main.json
|
||||
.cache
|
||||
@@ -31,4 +17,5 @@ config/state/queue_state.json
|
||||
output.log
|
||||
queue_state.json
|
||||
search_demo.py
|
||||
celery_worker.log
|
||||
celery_worker.log
|
||||
static/js/*
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -33,4 +33,5 @@ queue_state.json
|
||||
search_demo.py
|
||||
celery_worker.log
|
||||
logs/spotizerr.log
|
||||
/.venv
|
||||
/.venv
|
||||
static/js
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -10,6 +10,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
git \
|
||||
ffmpeg \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -22,6 +24,14 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Install TypeScript globally
|
||||
RUN npm install -g typescript
|
||||
|
||||
# Compile TypeScript
|
||||
# tsc will use tsconfig.json from the current directory (/app)
|
||||
# It will read from /app/src/js and output to /app/static/js
|
||||
RUN tsc
|
||||
|
||||
# Create necessary directories with proper permissions
|
||||
RUN mkdir -p downloads config creds logs && \
|
||||
chmod 777 downloads config creds logs
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
- ./logs:/app/logs # <-- Volume for persistent logs
|
||||
ports:
|
||||
- 7171:7171
|
||||
image: cooldockerizer93/spotizerr:dev
|
||||
image: test
|
||||
container_name: spotizerr-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { downloadQueue } from './queue.js';
|
||||
|
||||
// Interfaces for validator data
|
||||
interface SpotifyValidatorData {
|
||||
username: string;
|
||||
credentials?: string; // Credentials might be optional if only username is used as an identifier
|
||||
}
|
||||
|
||||
interface SpotifySearchValidatorData {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
}
|
||||
|
||||
interface DeezerValidatorData {
|
||||
arl: string;
|
||||
}
|
||||
|
||||
const serviceConfig: Record<string, any> = {
|
||||
spotify: {
|
||||
fields: [
|
||||
{ id: 'username', label: 'Username', type: 'text' },
|
||||
{ id: 'credentials', label: 'Credentials', type: 'text' }
|
||||
{ id: 'credentials', label: 'Credentials', type: 'text' } // Assuming this is password/token
|
||||
],
|
||||
validator: (data) => ({
|
||||
validator: (data: SpotifyValidatorData) => ({ // Typed data
|
||||
username: data.username,
|
||||
credentials: data.credentials
|
||||
}),
|
||||
@@ -15,7 +30,7 @@ const serviceConfig: Record<string, any> = {
|
||||
{ id: 'client_id', label: 'Client ID', type: 'text' },
|
||||
{ id: 'client_secret', label: 'Client Secret', type: 'password' }
|
||||
],
|
||||
searchValidator: (data) => ({
|
||||
searchValidator: (data: SpotifySearchValidatorData) => ({ // Typed data
|
||||
client_id: data.client_id,
|
||||
client_secret: data.client_secret
|
||||
})
|
||||
@@ -24,7 +39,7 @@ const serviceConfig: Record<string, any> = {
|
||||
fields: [
|
||||
{ id: 'arl', label: 'ARL', type: 'text' }
|
||||
],
|
||||
validator: (data) => ({
|
||||
validator: (data: DeezerValidatorData) => ({ // Typed data
|
||||
arl: data.arl
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,75 @@
|
||||
// main.ts
|
||||
import { downloadQueue } from './queue.js';
|
||||
|
||||
// Define interfaces for API data and search results
|
||||
interface Image {
|
||||
url: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
id?: string; // Artist ID might not always be present in search results for track artists
|
||||
name: string;
|
||||
external_urls?: { spotify?: string };
|
||||
genres?: string[]; // For artist type results
|
||||
}
|
||||
|
||||
interface Album {
|
||||
id?: string; // Album ID might not always be present
|
||||
name: string;
|
||||
images?: Image[];
|
||||
album_type?: string; // Used in startDownload
|
||||
artists?: Artist[]; // Album can have artists too
|
||||
total_tracks?: number;
|
||||
release_date?: string;
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
interface Track {
|
||||
id: string;
|
||||
name: string;
|
||||
artists: Artist[];
|
||||
album: Album;
|
||||
duration_ms?: number;
|
||||
explicit?: boolean;
|
||||
external_urls: { spotify: string };
|
||||
href?: string; // Some spotify responses use href
|
||||
}
|
||||
|
||||
interface Playlist {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: { display_name?: string; id?: string };
|
||||
images?: Image[];
|
||||
tracks: { total: number }; // Simplified for search results
|
||||
external_urls: { spotify: string };
|
||||
href?: string; // Some spotify responses use href
|
||||
explicit?: boolean; // Playlists themselves aren't explicit, but items can be
|
||||
}
|
||||
|
||||
// Specific item types for search results
|
||||
interface TrackResultItem extends Track {}
|
||||
interface AlbumResultItem extends Album { id: string; images?: Image[]; explicit?: boolean; external_urls: { spotify: string }; href?: string; }
|
||||
interface PlaylistResultItem extends Playlist {}
|
||||
interface ArtistResultItem extends Artist { id: string; images?: Image[]; explicit?: boolean; external_urls: { spotify: string }; href?: string; followers?: { total: number }; }
|
||||
|
||||
// Union type for any search result item
|
||||
type SearchResultItem = TrackResultItem | AlbumResultItem | PlaylistResultItem | ArtistResultItem;
|
||||
|
||||
// Interface for the API response structure
|
||||
interface SearchResponse {
|
||||
items: SearchResultItem[];
|
||||
// Add other top-level properties from the search API if needed (e.g., total, limit, offset)
|
||||
}
|
||||
|
||||
// Interface for the item passed to downloadQueue.download
|
||||
interface DownloadQueueItem {
|
||||
name: string;
|
||||
artist?: string;
|
||||
album?: { name: string; album_type?: string };
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM elements
|
||||
const searchInput = document.getElementById('searchInput') as HTMLInputElement | null;
|
||||
@@ -106,7 +175,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json() as SearchResponse; // Assert type for API response
|
||||
|
||||
// Hide loading indicator
|
||||
showLoading(false);
|
||||
@@ -164,7 +233,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
/**
|
||||
* Filters out items with null/undefined essential display parameters based on search type
|
||||
*/
|
||||
function filterValidItems(items: any[], type: string) {
|
||||
function filterValidItems(items: SearchResultItem[], type: string): SearchResultItem[] {
|
||||
if (!items) return [];
|
||||
|
||||
return items.filter(item => {
|
||||
@@ -172,59 +241,59 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!item) return false;
|
||||
|
||||
// Skip explicit content if filter is enabled
|
||||
if (downloadQueue.isExplicitFilterEnabled() && item.explicit === true) {
|
||||
if (downloadQueue.isExplicitFilterEnabled() && ('explicit' in item && item.explicit === true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check essential parameters based on search type
|
||||
switch (type) {
|
||||
case 'track':
|
||||
// For tracks, we need name, artists, and album
|
||||
const trackItem = item as TrackResultItem;
|
||||
return (
|
||||
item.name &&
|
||||
item.artists &&
|
||||
item.artists.length > 0 &&
|
||||
item.artists[0] &&
|
||||
item.artists[0].name &&
|
||||
item.album &&
|
||||
item.album.name &&
|
||||
item.external_urls &&
|
||||
item.external_urls.spotify
|
||||
trackItem.name &&
|
||||
trackItem.artists &&
|
||||
trackItem.artists.length > 0 &&
|
||||
trackItem.artists[0] &&
|
||||
trackItem.artists[0].name &&
|
||||
trackItem.album &&
|
||||
trackItem.album.name &&
|
||||
trackItem.external_urls &&
|
||||
trackItem.external_urls.spotify
|
||||
);
|
||||
|
||||
case 'album':
|
||||
// For albums, we need name, artists, and cover image
|
||||
const albumItem = item as AlbumResultItem;
|
||||
return (
|
||||
item.name &&
|
||||
item.artists &&
|
||||
item.artists.length > 0 &&
|
||||
item.artists[0] &&
|
||||
item.artists[0].name &&
|
||||
item.external_urls &&
|
||||
item.external_urls.spotify
|
||||
albumItem.name &&
|
||||
albumItem.artists &&
|
||||
albumItem.artists.length > 0 &&
|
||||
albumItem.artists[0] &&
|
||||
albumItem.artists[0].name &&
|
||||
albumItem.external_urls &&
|
||||
albumItem.external_urls.spotify
|
||||
);
|
||||
|
||||
case 'playlist':
|
||||
// For playlists, we need name, owner, and tracks
|
||||
const playlistItem = item as PlaylistResultItem;
|
||||
return (
|
||||
item.name &&
|
||||
item.owner &&
|
||||
item.owner.display_name &&
|
||||
item.tracks &&
|
||||
item.external_urls &&
|
||||
item.external_urls.spotify
|
||||
playlistItem.name &&
|
||||
playlistItem.owner &&
|
||||
playlistItem.owner.display_name &&
|
||||
playlistItem.tracks &&
|
||||
playlistItem.external_urls &&
|
||||
playlistItem.external_urls.spotify
|
||||
);
|
||||
|
||||
case 'artist':
|
||||
// For artists, we need name
|
||||
const artistItem = item as ArtistResultItem;
|
||||
return (
|
||||
item.name &&
|
||||
item.external_urls &&
|
||||
item.external_urls.spotify
|
||||
artistItem.name &&
|
||||
artistItem.external_urls &&
|
||||
artistItem.external_urls.spotify
|
||||
);
|
||||
|
||||
default:
|
||||
// Default case - just check if the item exists
|
||||
// Default case - just check if the item exists (already handled by `if (!item) return false;`)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -233,7 +302,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
/**
|
||||
* Attaches download handlers to result cards
|
||||
*/
|
||||
function attachDownloadListeners(items: any[]) {
|
||||
function attachDownloadListeners(items: SearchResultItem[]) {
|
||||
document.querySelectorAll('.download-btn').forEach((btnElm) => {
|
||||
const btn = btnElm as HTMLButtonElement;
|
||||
btn.addEventListener('click', (e: Event) => {
|
||||
@@ -262,10 +331,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Prepare metadata for the download
|
||||
const metadata = {
|
||||
name: item.name || 'Unknown',
|
||||
artist: item.artists ? item.artists[0]?.name : undefined
|
||||
};
|
||||
let metadata: DownloadQueueItem;
|
||||
if (currentSearchType === 'track') {
|
||||
const trackItem = item as TrackResultItem;
|
||||
metadata = {
|
||||
name: trackItem.name || 'Unknown',
|
||||
artist: trackItem.artists ? trackItem.artists[0]?.name : undefined,
|
||||
album: trackItem.album ? { name: trackItem.album.name, album_type: trackItem.album.album_type } : undefined
|
||||
};
|
||||
} else if (currentSearchType === 'album') {
|
||||
const albumItem = item as AlbumResultItem;
|
||||
metadata = {
|
||||
name: albumItem.name || 'Unknown',
|
||||
artist: albumItem.artists ? albumItem.artists[0]?.name : undefined,
|
||||
album: { name: albumItem.name, album_type: albumItem.album_type}
|
||||
};
|
||||
} else if (currentSearchType === 'playlist') {
|
||||
const playlistItem = item as PlaylistResultItem;
|
||||
metadata = {
|
||||
name: playlistItem.name || 'Unknown',
|
||||
// artist for playlist is owner
|
||||
artist: playlistItem.owner?.display_name
|
||||
};
|
||||
} else if (currentSearchType === 'artist') {
|
||||
const artistItem = item as ArtistResultItem;
|
||||
metadata = {
|
||||
name: artistItem.name || 'Unknown',
|
||||
artist: artistItem.name // For artist type, artist is the item name itself
|
||||
};
|
||||
} else {
|
||||
metadata = { name: item.name || 'Unknown' }; // Fallback
|
||||
}
|
||||
|
||||
// Disable the button and update text
|
||||
btn.disabled = true;
|
||||
@@ -278,7 +374,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Start the download
|
||||
startDownload(url, currentSearchType, metadata, item.album ? item.album.album_type : null)
|
||||
startDownload(url, currentSearchType, metadata,
|
||||
(item as AlbumResultItem).album_type || ((item as TrackResultItem).album ? (item as TrackResultItem).album.album_type : null))
|
||||
.then(() => {
|
||||
// For artists, show how many albums were queued
|
||||
if (currentSearchType === 'artist') {
|
||||
@@ -301,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
/**
|
||||
* Starts the download process via API
|
||||
*/
|
||||
async function startDownload(url: string, type: string, item: any, albumType: string | null) {
|
||||
async function startDownload(url: string, type: string, item: DownloadQueueItem, albumType: string | null | undefined) {
|
||||
if (!url || !type) {
|
||||
showError('Missing URL or type for download');
|
||||
return;
|
||||
@@ -385,7 +482,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
/**
|
||||
* Creates a result card element
|
||||
*/
|
||||
function createResultCard(item: any, type: string, index: number): HTMLDivElement {
|
||||
function createResultCard(item: SearchResultItem, type: string, index: number): HTMLDivElement {
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = 'result-card';
|
||||
|
||||
@@ -394,10 +491,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Get the appropriate image URL
|
||||
let imageUrl = '/static/images/placeholder.jpg';
|
||||
if (item.album && item.album.images && item.album.images.length > 0) {
|
||||
imageUrl = item.album.images[0].url;
|
||||
} else if (item.images && item.images.length > 0) {
|
||||
imageUrl = item.images[0].url;
|
||||
// Type guards to safely access images
|
||||
if (type === 'album' || type === 'artist') {
|
||||
const albumOrArtistItem = item as AlbumResultItem | ArtistResultItem;
|
||||
if (albumOrArtistItem.images && albumOrArtistItem.images.length > 0) {
|
||||
imageUrl = albumOrArtistItem.images[0].url;
|
||||
}
|
||||
} else if (type === 'track') {
|
||||
const trackItem = item as TrackResultItem;
|
||||
if (trackItem.album && trackItem.album.images && trackItem.album.images.length > 0) {
|
||||
imageUrl = trackItem.album.images[0].url;
|
||||
}
|
||||
} else if (type === 'playlist') {
|
||||
const playlistItem = item as PlaylistResultItem;
|
||||
if (playlistItem.images && playlistItem.images.length > 0) {
|
||||
imageUrl = playlistItem.images[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the appropriate details based on type
|
||||
@@ -406,20 +515,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
switch (type) {
|
||||
case 'track':
|
||||
subtitle = item.artists ? item.artists.map(a => a.name).join(', ') : 'Unknown Artist';
|
||||
details = item.album ? `<span>${item.album.name}</span><span class="duration">${msToMinutesSeconds(item.duration_ms)}</span>` : '';
|
||||
{
|
||||
const trackItem = item as TrackResultItem;
|
||||
subtitle = trackItem.artists ? trackItem.artists.map((a: Artist) => a.name).join(', ') : 'Unknown Artist';
|
||||
details = trackItem.album ? `<span>${trackItem.album.name}</span><span class="duration">${msToMinutesSeconds(trackItem.duration_ms)}</span>` : '';
|
||||
}
|
||||
break;
|
||||
case 'album':
|
||||
subtitle = item.artists ? item.artists.map(a => a.name).join(', ') : 'Unknown Artist';
|
||||
details = `<span>${item.total_tracks || 0} tracks</span><span>${item.release_date ? new Date(item.release_date).getFullYear() : ''}</span>`;
|
||||
{
|
||||
const albumItem = item as AlbumResultItem;
|
||||
subtitle = albumItem.artists ? albumItem.artists.map((a: Artist) => a.name).join(', ') : 'Unknown Artist';
|
||||
details = `<span>${albumItem.total_tracks || 0} tracks</span><span>${albumItem.release_date ? new Date(albumItem.release_date).getFullYear() : ''}</span>`;
|
||||
}
|
||||
break;
|
||||
case 'playlist':
|
||||
subtitle = `By ${item.owner ? item.owner.display_name : 'Unknown'}`;
|
||||
details = `<span>${item.tracks && item.tracks.total ? item.tracks.total : 0} tracks</span>`;
|
||||
{
|
||||
const playlistItem = item as PlaylistResultItem;
|
||||
subtitle = `By ${playlistItem.owner ? playlistItem.owner.display_name : 'Unknown'}`;
|
||||
details = `<span>${playlistItem.tracks && playlistItem.tracks.total ? playlistItem.tracks.total : 0} tracks</span>`;
|
||||
}
|
||||
break;
|
||||
case 'artist':
|
||||
subtitle = 'Artist';
|
||||
details = item.genres ? `<span>${item.genres.slice(0, 2).join(', ')}</span>` : '';
|
||||
{
|
||||
const artistItem = item as ArtistResultItem;
|
||||
subtitle = 'Artist';
|
||||
details = artistItem.genres ? `<span>${artistItem.genres.slice(0, 2).join(', ')}</span>` : '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,68 @@
|
||||
// Import the downloadQueue singleton from your working queue.js implementation.
|
||||
import { downloadQueue } from './queue.js';
|
||||
|
||||
// Define interfaces for API data
|
||||
interface Image {
|
||||
url: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
id: string;
|
||||
name: string;
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
name: string;
|
||||
images?: Image[];
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
interface Track {
|
||||
id: string;
|
||||
name: string;
|
||||
artists: Artist[];
|
||||
album: Album;
|
||||
duration_ms: number;
|
||||
explicit: boolean;
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
interface PlaylistItem {
|
||||
track: Track | null;
|
||||
// Add other playlist item properties like added_at, added_by if needed
|
||||
}
|
||||
|
||||
interface Playlist {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
owner: {
|
||||
display_name?: string;
|
||||
id?: string;
|
||||
};
|
||||
images: Image[];
|
||||
tracks: {
|
||||
items: PlaylistItem[];
|
||||
total: number;
|
||||
};
|
||||
followers?: {
|
||||
total: number;
|
||||
};
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
interface DownloadQueueItem {
|
||||
name: string;
|
||||
artist?: string; // Can be a simple string for the queue
|
||||
album?: { name: string }; // Match QueueItem's album structure
|
||||
owner?: string; // For playlists, owner can be a string
|
||||
// Add any other properties your item might have, compatible with QueueItem
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Parse playlist ID from URL
|
||||
const pathSegments = window.location.pathname.split('/');
|
||||
@@ -15,7 +77,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
return response.json();
|
||||
return response.json() as Promise<Playlist>;
|
||||
})
|
||||
.then(data => renderPlaylist(data))
|
||||
.catch(error => {
|
||||
@@ -34,7 +96,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* Renders playlist header and tracks.
|
||||
*/
|
||||
function renderPlaylist(playlist: any) {
|
||||
function renderPlaylist(playlist: Playlist) {
|
||||
// Hide loading and error messages
|
||||
const loadingEl = document.getElementById('loading');
|
||||
if (loadingEl) loadingEl.classList.add('hidden');
|
||||
@@ -80,7 +142,7 @@ function renderPlaylist(playlist: any) {
|
||||
// Check if any track in the playlist is explicit when filter is enabled
|
||||
let hasExplicitTrack = false;
|
||||
if (isExplicitFilterEnabled && playlist.tracks?.items) {
|
||||
hasExplicitTrack = playlist.tracks.items.some(item => item?.track && item.track.explicit);
|
||||
hasExplicitTrack = playlist.tracks.items.some((item: PlaylistItem) => item?.track && item.track.explicit);
|
||||
}
|
||||
|
||||
// --- Add "Download Whole Playlist" Button ---
|
||||
@@ -178,7 +240,7 @@ function renderPlaylist(playlist: any) {
|
||||
tracksList.innerHTML = ''; // Clear any existing content
|
||||
|
||||
if (playlist.tracks?.items) {
|
||||
playlist.tracks.items.forEach((item, index) => {
|
||||
playlist.tracks.items.forEach((item: PlaylistItem, index: number) => {
|
||||
if (!item || !item.track) return; // Skip null/undefined tracks
|
||||
|
||||
const track = item.track;
|
||||
@@ -281,7 +343,10 @@ function attachDownloadListeners() {
|
||||
const name = currentTarget.dataset.name || extractName(url) || 'Unknown';
|
||||
// Remove the button immediately after click.
|
||||
currentTarget.remove();
|
||||
startDownload(url, type, { name }, ''); // Added empty string for albumType
|
||||
// For individual track downloads, we might not have album/artist name readily here.
|
||||
// The queue.ts download method should be robust enough or we might need to fetch more data.
|
||||
// For now, pass what we have.
|
||||
startDownload(url, type, { name }, ''); // Pass name, artist/album are optional in DownloadQueueItem
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -289,7 +354,7 @@ function attachDownloadListeners() {
|
||||
/**
|
||||
* Initiates the whole playlist download by calling the playlist endpoint.
|
||||
*/
|
||||
async function downloadWholePlaylist(playlist: any) {
|
||||
async function downloadWholePlaylist(playlist: Playlist) {
|
||||
if (!playlist) {
|
||||
throw new Error('Invalid playlist data');
|
||||
}
|
||||
@@ -301,7 +366,11 @@ async function downloadWholePlaylist(playlist: any) {
|
||||
|
||||
try {
|
||||
// Use the centralized downloadQueue.download method
|
||||
await downloadQueue.download(url, 'playlist', { name: playlist.name || 'Unknown Playlist' });
|
||||
await downloadQueue.download(url, 'playlist', {
|
||||
name: playlist.name || 'Unknown Playlist',
|
||||
owner: playlist.owner?.display_name // Pass owner as a string
|
||||
// total_tracks can also be passed if QueueItem supports it directly
|
||||
});
|
||||
// Make the queue visible after queueing
|
||||
downloadQueue.toggleVisibility(true);
|
||||
} catch (error: any) {
|
||||
@@ -315,15 +384,15 @@ async function downloadWholePlaylist(playlist: any) {
|
||||
* adding a 20ms delay between each album download and updating the button
|
||||
* with the progress (queued_albums/total_albums).
|
||||
*/
|
||||
async function downloadPlaylistAlbums(playlist: any) {
|
||||
async function downloadPlaylistAlbums(playlist: Playlist) {
|
||||
if (!playlist?.tracks?.items) {
|
||||
showError('No tracks found in this playlist.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a map of unique albums (using album ID as the key).
|
||||
const albumMap = new Map();
|
||||
playlist.tracks.items.forEach(item => {
|
||||
const albumMap = new Map<string, Album>();
|
||||
playlist.tracks.items.forEach((item: PlaylistItem) => {
|
||||
if (!item?.track?.album) return;
|
||||
|
||||
const album = item.track.album;
|
||||
@@ -359,7 +428,11 @@ async function downloadPlaylistAlbums(playlist: any) {
|
||||
await downloadQueue.download(
|
||||
albumUrl,
|
||||
'album',
|
||||
{ name: album.name || 'Unknown Album' }
|
||||
{
|
||||
name: album.name || 'Unknown Album',
|
||||
// If artist information is available on album objects from playlist, pass it
|
||||
// artist: album.artists?.[0]?.name
|
||||
}
|
||||
);
|
||||
|
||||
// Update button text with current progress.
|
||||
@@ -387,7 +460,7 @@ async function downloadPlaylistAlbums(playlist: any) {
|
||||
/**
|
||||
* Starts the download process using the centralized download method from the queue.
|
||||
*/
|
||||
async function startDownload(url: string, type: string, item: any, albumType?: string) {
|
||||
async function startDownload(url: string, type: string, item: DownloadQueueItem, albumType?: string) {
|
||||
if (!url || !type) {
|
||||
showError('Missing URL or type for download');
|
||||
return;
|
||||
@@ -826,7 +826,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
const entries = Object.values(this.queueEntries);
|
||||
|
||||
// Sorting: errors/canceled first (group 0), ongoing next (group 1), queued last (group 2, sorted by position).
|
||||
entries.sort((a, b) => {
|
||||
entries.sort((a: QueueEntry, b: QueueEntry) => {
|
||||
const getGroup = (entry: QueueEntry) => { // Add type
|
||||
if (entry.lastStatus && (entry.lastStatus.status === "error" || entry.lastStatus.status === "cancel")) {
|
||||
return 0;
|
||||
@@ -881,7 +881,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
if (visibleItems.length === 0) {
|
||||
// No items in container, append all visible entries
|
||||
container.innerHTML = ''; // Clear any empty state
|
||||
visibleEntries.forEach(entry => {
|
||||
visibleEntries.forEach((entry: QueueEntry) => {
|
||||
// We no longer automatically start monitoring here
|
||||
// Monitoring is now explicitly started by the methods that create downloads
|
||||
container.appendChild(entry.element);
|
||||
@@ -890,7 +890,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
// Container already has items, update more efficiently
|
||||
|
||||
// Create a map of current DOM elements by queue ID
|
||||
const existingElementMap = {};
|
||||
const existingElementMap: { [key: string]: HTMLElement } = {};
|
||||
visibleItems.forEach(el => {
|
||||
const queueId = (el.querySelector('.cancel-btn') as HTMLElement | null)?.dataset.queueid; // Optional chaining
|
||||
if (queueId) existingElementMap[queueId] = el as HTMLElement; // Cast to HTMLElement
|
||||
@@ -900,7 +900,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
container.innerHTML = '';
|
||||
|
||||
// Add visible entries in correct order
|
||||
visibleEntries.forEach(entry => {
|
||||
visibleEntries.forEach((entry: QueueEntry) => {
|
||||
// We no longer automatically start monitoring here
|
||||
container.appendChild(entry.element);
|
||||
|
||||
@@ -931,7 +931,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
/* Checks if an entry is visible in the queue display. */
|
||||
isEntryVisible(queueId: string): boolean { // Add return type
|
||||
const entries = Object.values(this.queueEntries);
|
||||
entries.sort((a, b) => {
|
||||
entries.sort((a: QueueEntry, b: QueueEntry) => {
|
||||
const getGroup = (entry: QueueEntry) => { // Add type
|
||||
if (entry.lastStatus && (entry.lastStatus.status === "error" || entry.lastStatus.status === "cancel")) {
|
||||
return 0;
|
||||
@@ -954,7 +954,7 @@ createQueueItem(item: QueueItem, type: string, prgFile: string, queueId: string)
|
||||
return a.lastUpdated - b.lastUpdated;
|
||||
}
|
||||
});
|
||||
const index = entries.findIndex(e => e.uniqueId === queueId);
|
||||
const index = entries.findIndex((e: QueueEntry) => e.uniqueId === queueId);
|
||||
return index >= 0 && index < this.visibleCount;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017", // Specify ECMAScript target version
|
||||
"target": "ES2017", // Specify ECMAScript target version
|
||||
"module": "ES2020", // Specify module code generation
|
||||
"strict": true, // Enable all strict type-checking options
|
||||
"noImplicitAny": false, // Allow implicit 'any' types
|
||||
"esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules
|
||||
"skipLibCheck": true, // Skip type checking of declaration files
|
||||
"forceConsistentCasingInFileNames": true // Disallow inconsistently-cased references to the same file.
|
||||
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
|
||||
"outDir": "./static/js",
|
||||
"rootDir": "./src/js"
|
||||
},
|
||||
"include": [
|
||||
"static/js/**/*.ts" // Specifies the TypeScript files to be included in compilation
|
||||
"src/js/**/*.ts",
|
||||
"src/js/album.ts",
|
||||
"src/js/artist.ts",
|
||||
"src/js/config.ts",
|
||||
"src/js/main.ts",
|
||||
"src/js/playlist.ts",
|
||||
"src/js/queue.ts",
|
||||
"src/js/track.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules" // Specifies an array of filenames or patterns that should be skipped when resolving include.
|
||||
|
||||
Reference in New Issue
Block a user