diff --git a/backend/debug-html.js b/backend/debug-html.js new file mode 100644 index 0000000..d424c6c --- /dev/null +++ b/backend/debug-html.js @@ -0,0 +1,67 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); + +async function debugHTML() { + try { + console.log('Fetching Shazam Country Chart HTML...'); + + const response = await axios.get('https://www.shazam.com/charts/genre/united-states/country', { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Cache-Control': 'max-age=0' + }, + timeout: 30000 + }); + + const html = response.data; + const $ = cheerio.load(html); + + console.log('HTML length:', html.length); + + // Look for list elements + const ulElements = $('ul'); + console.log('Found', ulElements.length, 'ul elements'); + + ulElements.each((i, ul) => { + const $ul = $(ul); + const liElements = $ul.find('li'); + console.log(`UL ${i + 1}: Found ${liElements.length} li elements`); + + if (liElements.length > 0) { + liElements.each((j, li) => { + const $li = $(li); + const text = $li.text().trim(); + if (text.length > 0) { + console.log(` LI ${j + 1}: "${text}"`); + } + }); + } + }); + + // Look for elements with numbers + console.log('\nLooking for elements with numbers...'); + $('*').each((i, el) => { + const $el = $(el); + const text = $el.text().trim(); + + // Look for text that starts with a number and contains song-like content + if (text.match(/^\d+/) && text.length > 10 && text.length < 200) { + console.log(`Element ${i}: "${text}"`); + } + }); + + } catch (error) { + console.error('Error:', error.message); + } +} + +debugHTML(); \ No newline at end of file diff --git a/backend/debug-shazam.js b/backend/debug-shazam.js new file mode 100644 index 0000000..368a092 --- /dev/null +++ b/backend/debug-shazam.js @@ -0,0 +1,14 @@ +const ShazamService = require('./src/models/shazamService'); + +async function debugShazam() { + try { + console.log('Testing Shazam Country Chart...'); + const data = await ShazamService.getChartData('country'); + console.log('Found', data.length, 'songs:'); + console.log(JSON.stringify(data, null, 2)); + } catch (error) { + console.error('Error:', error.message); + } +} + +debugShazam(); \ No newline at end of file diff --git a/backend/src/controllers/chartController.js b/backend/src/controllers/chartController.js index 69906e3..d56b782 100644 --- a/backend/src/controllers/chartController.js +++ b/backend/src/controllers/chartController.js @@ -1,14 +1,23 @@ const ChartService = require('../models/chartService'); +const KworbService = require('../models/kworbService'); +const ShazamService = require('../models/shazamService'); // Get available dates for a specific year const getChartDates = async (req, res) => { try { const { year } = req.params; + const source = req.query.source || 'musicchartsarchive'; + let dates; - // Validate year parameter - const validatedYear = ChartService.validateYear(year); - - const dates = await ChartService.getAvailableDates(validatedYear); + if (source === 'kworb') { + dates = await KworbService.getAvailableDates(year); + } else if (source === 'shazam') { + dates = await ShazamService.getAvailableDates(year); + } else { + // Validate year parameter + const validatedYear = ChartService.validateYear(year); + dates = await ChartService.getAvailableDates(validatedYear); + } res.json(dates); } catch (error) { console.error('Error in getChartDates:', error); @@ -23,6 +32,8 @@ const getChartDates = async (req, res) => { const getChartData = async (req, res) => { try { const { date } = req.params; + const source = req.query.source || 'musicchartsarchive'; + const chartType = req.query.chartType || 'top-200'; // For Shazam if (!date) { return res.status(400).json({ @@ -30,11 +41,17 @@ const getChartData = async (req, res) => { message: 'Date parameter is required' }); } - - // Validate date format - ChartService.validateDate(date); - - const data = await ChartService.getChartData(date); + + let data; + if (source === 'kworb') { + data = await KworbService.getChartData(date); + } else if (source === 'shazam') { + data = await ShazamService.getChartData(chartType); + } else { + // Validate date format + ChartService.validateDate(date); + data = await ChartService.getChartData(date); + } res.json(data); } catch (error) { console.error('Error in getChartData:', error); @@ -49,11 +66,19 @@ const getChartData = async (req, res) => { const getYearlyTopSongs = async (req, res) => { try { const { year } = req.params; + const source = req.query.source || 'musicchartsarchive'; + const chartType = req.query.chartType || 'top-200'; // For Shazam - // Validate year parameter - const validatedYear = ChartService.validateYear(year); - - const yearlySongs = await ChartService.getYearlyTopSongs(validatedYear); + let yearlySongs; + if (source === 'kworb') { + yearlySongs = await KworbService.getYearlyTopSongs(year); + } else if (source === 'shazam') { + yearlySongs = await ShazamService.getYearlyTopSongs(year, chartType); + } else { + // Validate year parameter + const validatedYear = ChartService.validateYear(year); + yearlySongs = await ChartService.getYearlyTopSongs(validatedYear); + } res.json(yearlySongs); } catch (error) { console.error('Error in getYearlyTopSongs:', error); @@ -64,8 +89,23 @@ const getYearlyTopSongs = async (req, res) => { } }; +// Get available Shazam chart types +const getShazamChartTypes = async (req, res) => { + try { + const chartTypes = ShazamService.getAvailableCharts(); + res.json(chartTypes); + } catch (error) { + console.error('Error in getShazamChartTypes:', error); + res.status(400).json({ + error: 'Failed to fetch Shazam chart types', + message: error.message + }); + } +}; + module.exports = { getChartDates, getChartData, - getYearlyTopSongs + getYearlyTopSongs, + getShazamChartTypes }; \ No newline at end of file diff --git a/backend/src/models/kworbService.js b/backend/src/models/kworbService.js new file mode 100644 index 0000000..ab8b22e --- /dev/null +++ b/backend/src/models/kworbService.js @@ -0,0 +1,165 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); + +const BASE_URL = 'https://kworb.net/apple_songs/'; + +class KworbService { + // Get available dates from the archive (2022 and forward) + static async getAvailableDates(year) { + if (parseInt(year) < 2022) return []; + const archiveUrl = BASE_URL + 'archive/'; + console.log('[kworb] Fetching archive:', archiveUrl); + const res = await axios.get(archiveUrl); + const $ = cheerio.load(res.data); + const dates = []; + $('a').each((i, el) => { + const href = $(el).attr('href'); + // kworb archive links are in the format YYYYMMDD.html + const match = href && href.match(/(\d{8})\.html/); + if (match) { + // Convert to YYYY-MM-DD for API + const date = match[1].replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + const y = parseInt(date.substring(0, 4)); + if (y >= 2022 && date.startsWith(year)) { + dates.push({ + date, + formattedDate: date // You can format as needed + }); + } + } + }); + // Optionally add 'today' as the current chart if year is current year + const now = new Date(); + if (parseInt(year) === now.getFullYear()) { + dates.unshift({ + date: 'today', + formattedDate: 'Today' + }); + } + // Sort dates ascending + dates.sort((a, b) => a.date.localeCompare(b.date)); + console.log('[kworb] Available dates for', year, ':', dates.map(d => d.date)); + return dates; + } + + // Get chart data for a specific date + static async getChartData(date) { + let url; + if (date === 'today') { + url = BASE_URL + 'index.html'; + } else { + // kworb archive URLs use YYYYMMDD (no dashes) + const dateNoDash = date.replace(/-/g, ''); + url = BASE_URL + 'archive/' + dateNoDash + '.html'; + } + console.log('[kworb] Fetching chart data:', url); + const res = await axios.get(url); + const $ = cheerio.load(res.data); + let songs = []; + $('table.sortable tbody tr').each((i, el) => { + const tds = $(el).find('td'); + if (tds.length >= 13) { + const us = parseInt($(tds[9]).text().trim()); + if (isNaN(us)) return; // Only include songs with a valid US position + const artistAndTitle = $(tds[2]).text().trim(); + // Split "Artist - Title" + const [artist, ...titleParts] = artistAndTitle.split(' - '); + const title = titleParts.join(' - '); + if (artist && title) { + songs.push({ + order: us, // Use US chart position as the order + artist: artist.trim(), + title: title.trim() + }); + } + } + }); + // Sort by US ranking (ascending) and take top 50 + songs = songs.sort((a, b) => a.order - b.order).slice(0, 50); + console.log('[kworb] Found', songs.length, 'songs with US ranking'); + return songs; + } + + // Get yearly top songs for a year (sample charts throughout the year, aggregate by best US position) + static async getYearlyTopSongs(year) { + const dates = await this.getAvailableDates(year); + if (dates.length === 0) return []; + + // Sample charts more frequently - every 3-4 days instead of weekly + const sampledDates = []; + const step = Math.max(1, Math.floor(dates.length / 52)); // Aim for ~52 samples per year + + for (let i = 0; i < dates.length; i += step) { + sampledDates.push(dates[i].date); + } + + // Also include the last date if not already included + if (sampledDates.length > 0 && sampledDates[sampledDates.length - 1] !== dates[dates.length - 1].date) { + sampledDates.push(dates[dates.length - 1].date); + } + + console.log('[kworb] Sampled dates for yearly top:', sampledDates.length, 'charts'); + console.log('[kworb] Sample dates:', sampledDates.slice(0, 10), '...'); + // Aggregate songs by title+artist, track best US position and appearances + const songMap = {}; + let totalSongsProcessed = 0; + + for (const date of sampledDates) { + try { + const chart = await this.getChartData(date); + totalSongsProcessed += chart.length; + + chart.forEach(song => { + if (!song.title || !song.artist) return; + const key = song.title + '|' + song.artist; + if (!songMap[key]) { + songMap[key] = { + title: song.title, + artist: song.artist, + bestUS: song.order, + appearances: 0 + }; + } + if (song.order < songMap[key].bestUS) { + songMap[key].bestUS = song.order; + } + songMap[key].appearances++; + }); + } catch (error) { + console.log('[kworb] Error processing date', date, ':', error.message); + } + } + + console.log('[kworb] Processed', totalSongsProcessed, 'total songs across', sampledDates.length, 'charts'); + console.log('[kworb] Found', Object.keys(songMap).length, 'unique songs'); + // Sort by best US position (ascending), then appearances (descending), then title for consistency + let topSongs = Object.values(songMap) + .sort((a, b) => { + if (a.bestUS !== b.bestUS) return a.bestUS - b.bestUS; + if (a.appearances !== b.appearances) return b.appearances - a.appearances; + return a.title.localeCompare(b.title); // Consistent tie-breaker + }) + .slice(0, 50) + .map((song, index) => ({ + order: index + 1, // Create new ranking based on aggregated performance + title: song.title, + artist: song.artist, + appearances: song.appearances, + bestUS: song.bestUS // Keep the best US position for reference + })); + console.log('[kworb] Yearly top songs:', topSongs.length, 'songs'); + console.log('[kworb] Sample of yearly data:', topSongs.slice(0, 5).map(s => `${s.order}. ${s.title} - ${s.artist} (appearances: ${s.appearances}, best US: ${s.bestUS})`)); + return topSongs; + } +} + +// Helper: get ISO week number from date string (YYYY-MM-DD) +function getWeekOfYear(dateStr) { + const d = new Date(dateStr); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() + 4 - (d.getDay() || 7)); + const yearStart = new Date(d.getFullYear(), 0, 1); + return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); +} + +module.exports = KworbService; \ No newline at end of file diff --git a/backend/src/models/shazamService.js b/backend/src/models/shazamService.js new file mode 100644 index 0000000..0e37f55 --- /dev/null +++ b/backend/src/models/shazamService.js @@ -0,0 +1,694 @@ +const axios = require('axios'); +const cheerio = require('cheerio'); + +const BASE_URL = 'https://www.shazam.com/charts'; + +class ShazamService { + // Get available chart types + static getAvailableCharts() { + return [ + { id: 'top-200', name: 'Top 200', url: `${BASE_URL}/top-200/united-states` }, + { id: 'pop', name: 'Pop', url: `${BASE_URL}/genre/united-states/pop` }, + { id: 'hip-hop-rap', name: 'Hip-Hop/Rap', url: `${BASE_URL}/genre/united-states/hip-hop-rap` }, + { id: 'country', name: 'Country', url: `${BASE_URL}/genre/united-states/country` } + ]; + } + + // Get chart data for a specific chart type + static async getChartData(chartType = 'top-200') { + const charts = this.getAvailableCharts(); + const chart = charts.find(c => c.id === chartType); + + if (!chart) { + throw new Error(`Invalid chart type: ${chartType}. Available: ${charts.map(c => c.id).join(', ')}`); + } + + console.log('[shazam] Fetching chart data from:', chart.url); + return await this.scrapeHTMLData(chart.url); + } + + // Scrape HTML data from Shazam + static async scrapeHTMLData(url) { + console.log('[shazam] Scraping HTML from:', url); + + const response = await axios.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Cache-Control': 'max-age=0' + }, + timeout: 30000 + }); + + const html = response.data; + return this.parseHTML(html); + } + + // Parse HTML data based on Shazam's structure + static parseHTML(html) { + const songs = []; + const $ = cheerio.load(html); + + console.log('[shazam] Parsing HTML content...'); + console.log('[shazam] HTML length:', html.length); + + // Save raw HTML for debugging (first 5000 chars) + console.log('[shazam] Raw HTML preview:', html.substring(0, 5000)); + + // First, try the new Shazam-specific parsing method + console.log('[shazam] Trying Shazam-specific structure parsing...'); + this.extractSongsFromShazamStructure($, songs); + + // Also try the exact structure from the provided HTML + console.log('[shazam] Trying exact HTML structure parsing...'); + this.extractSongsFromExactStructure($, songs); + + // If no songs found, try the existing methods + if (songs.length === 0) { + console.log('[shazam] No songs found with Shazam structure, trying alternative methods...'); + + // Look for the specific chart list container class + const chartContainer = $('.ListShowMoreLess_container__t4TNB.page_chartList__aBclW'); + console.log('[shazam] Chart container found:', chartContainer.length); + + if (chartContainer.length === 0) { + console.log('[shazam] Chart container not found, trying alternative selectors...'); + + // Try alternative selectors + const alternativeSelectors = [ + '.page_chartList__aBclW', + '.ListShowMoreLess_container__t4TNB', + '[class*="chartList"]', + '[class*="ListShowMoreLess"]', + '[class*="chart"]', + '[class*="list"]' + ]; + + for (const selector of alternativeSelectors) { + const container = $(selector); + console.log('[shazam] Selector', selector, 'found:', container.length, 'elements'); + if (container.length > 0) { + console.log('[shazam] Found container with selector:', selector); + this.extractSongsFromContainer(container, songs); + break; + } + } + } else { + console.log('[shazam] Found chart container with specific class'); + this.extractSongsFromContainer(chartContainer, songs); + } + + // If still no songs, try extracting from image alt text + if (songs.length === 0) { + console.log('[shazam] No songs found in containers, trying image alt text...'); + this.extractSongsFromImages($, songs); + } + + // If still no songs, try extracting from JSON-LD structured data + if (songs.length === 0) { + console.log('[shazam] No songs found in images, trying JSON-LD data...'); + this.extractSongsFromJSONLD($, songs); + } + + // If still no songs, try extracting from list elements (ul/li) + if (songs.length === 0) { + console.log('[shazam] No songs found in JSON-LD, trying list elements...'); + this.extractSongsFromListElements($, songs); + } + + // If still no songs, try looking for Shazam-specific elements + if (songs.length === 0) { + console.log('[shazam] No songs found in list elements, trying Shazam-specific elements...'); + this.extractSongsFromShazamElements($, songs); + } + + // If still no songs, try a more generic approach + if (songs.length === 0) { + console.log('[shazam] No songs found in Shazam elements, trying generic approach...'); + this.extractSongsGeneric($, songs); + } + } + + // Sort by order and take top 50 + songs.sort((a, b) => a.order - b.order); + const topSongs = songs.slice(0, 50); + + console.log('[shazam] Final result:', topSongs.length, 'songs'); + + // Log some examples for debugging + if (topSongs.length > 0) { + console.log('[shazam] Sample songs:', topSongs.slice(0, 3)); + } else { + console.log('[shazam] No songs found. Checking for any numbers in text...'); + const textContent = $.text(); + const numberMatches = textContent.match(/\d+/g); + console.log('[shazam] Numbers found in text:', numberMatches ? numberMatches.slice(0, 10) : 'none'); + + // Look for any text that might contain song data + const lines = textContent.split('\n'); + console.log('[shazam] Text lines (first 20):', lines.slice(0, 20)); + } + + return topSongs; + } + + // Extract songs from a specific container + static extractSongsFromContainer(container, songs) { + console.log('[shazam] Extracting songs from container...'); + + // First, try to find chart items with more specific selectors + const chartItems = container.find('[class*="chart-item"], [class*="track-item"], [class*="song-item"], [class*="list-item"]'); + console.log('[shazam] Found chart items:', chartItems.length); + + if (chartItems.length > 0) { + chartItems.each((i, el) => { + const $el = cheerio.load(el); + const text = $el.text().trim(); + console.log('[shazam] Chart item text:', text.substring(0, 100)); + + // Try different patterns for song data + const patterns = [ + /^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" + /^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period) + /^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period) + /^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist" + /^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist" + /^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i // "1. Song Title by Artist" + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const order = parseInt(match[1]); + const title = match[2].trim(); + const artist = match[3].trim(); + + if (!isNaN(order) && title && artist && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order, title, artist }); + console.log('[shazam] Found song:', order, title, '-', artist); + } + } + break; // Found a match, no need to try other patterns + } + } + }); + } + + // If no chart items found, try looking for any elements with numbers and text + if (songs.length === 0) { + console.log('[shazam] No chart items found, trying generic elements...'); + + container.find('*').each((i, el) => { + const $el = cheerio.load(el); + const text = $el.text().trim(); + + // Only process elements with substantial text + if (text.length > 10 && text.length < 200) { + const patterns = [ + /^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" + /^(\d+)\s+(.+?)\s+(.+)$/, // "1 Song Title Artist" (no dash) + /^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period) + /^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/ // "1 - Song Title - Artist" + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const order = parseInt(match[1]); + const title = match[2].trim(); + const artist = match[3].trim(); + + if (!isNaN(order) && title && artist && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order, title, artist }); + console.log('[shazam] Found song (generic):', order, title, '-', artist); + } + } + break; // Found a match, no need to try other patterns + } + } + } + }); + } + } + + // Extract songs from image alt text (Shazam uses this for song info) + static extractSongsFromImages($, songs) { + let songCount = 0; + + // Look for images with alt text containing song information + $('img[alt*="by"]').each((i, el) => { + const alt = $(el).attr('alt'); + if (alt) { + // Pattern: "Album artwork for album titled Song Title by Artist" + // or "Listen to Song Title by Artist" + const patterns = [ + /album titled (.+?) by (.+)/i, + /Listen to (.+?) by (.+)/i, + /(.+?) by (.+)/i + ]; + + for (const pattern of patterns) { + const match = alt.match(pattern); + if (match) { + const title = match[1].trim(); + const artist = match[2].trim(); + + // Check if we already have this song + const exists = songs.some(s => s.title === title && s.artist === artist); + if (!exists) { + songCount++; + songs.push({ order: songCount, title, artist }); + console.log('[shazam] Found song from image:', songCount, title, '-', artist); + } + break; // Found a match, no need to try other patterns + } + } + } + }); + } + + // Extract songs from JSON-LD structured data + static extractSongsFromJSONLD($, songs) { + try { + // Look for JSON-LD script tags + $('script[type="application/ld+json"]').each((i, el) => { + const scriptContent = $(el).html(); + if (scriptContent) { + try { + const jsonData = JSON.parse(scriptContent); + console.log('[shazam] Found JSON-LD data:', typeof jsonData); + + // Handle different JSON-LD structures + if (jsonData['@graph'] && Array.isArray(jsonData['@graph'])) { + // Schema.org MusicPlaylist structure + jsonData['@graph'].forEach(item => { + if (item['@type'] === 'MusicRecording' && item.name && item.byArtist) { + const title = item.name; + const artist = typeof item.byArtist === 'object' ? item.byArtist.name : item.byArtist; + const position = item.position || songs.length + 1; + + const exists = songs.some(s => s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order: position, title, artist }); + console.log('[shazam] Found song from JSON-LD:', position, title, '-', artist); + } + } + }); + } else if (jsonData['@type'] === 'MusicPlaylist' && jsonData.track) { + // Direct playlist structure + jsonData.track.forEach((track, index) => { + if (track.name && track.byArtist) { + const title = track.name; + const artist = typeof track.byArtist === 'object' ? track.byArtist.name : track.byArtist; + const position = track.position || index + 1; + + const exists = songs.some(s => s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order: position, title, artist }); + console.log('[shazam] Found song from JSON-LD playlist:', position, title, '-', artist); + } + } + }); + } + } catch (parseError) { + console.log('[shazam] Failed to parse JSON-LD:', parseError.message); + } + } + }); + } catch (error) { + console.log('[shazam] Error extracting from JSON-LD:', error.message); + } + } + + // Extract songs from list elements (ul/li structure) + static extractSongsFromListElements($, songs) { + console.log('[shazam] Looking for list elements (ul/li)...'); + + // Look for all ul elements + $('ul').each((ulIndex, ul) => { + const $ul = $(ul); + const liElements = $ul.find('li'); + console.log('[shazam] Found ul with', liElements.length, 'li elements'); + + if (liElements.length > 0) { + liElements.each((liIndex, li) => { + const $li = $(li); + const text = $li.text().trim(); + console.log('[shazam] Li text:', text); + + // Try different patterns for song data in list items + const patterns = [ + /^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" + /^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period) + /^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period) + /^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist" + /^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist" + /^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i, // "1. Song Title by Artist" + /^(\d+)\s+(.+?)\s*[-–]\s*(.+)$/ // "1 Song Title - Artist" (flexible spacing) + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const order = parseInt(match[1]); + const title = match[2].trim(); + const artist = match[3].trim(); + + if (!isNaN(order) && title && artist && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order, title, artist }); + console.log('[shazam] Found song from list:', order, title, '-', artist); + } + } + break; // Found a match, no need to try other patterns + } + } + }); + } + }); + } + + // Extract songs from Shazam-specific elements + static extractSongsFromShazamElements($, songs) { + console.log('[shazam] Looking for Shazam-specific elements...'); + + // Look for elements with Shazam-specific classes or attributes + const shazamSelectors = [ + '[class*="track"]', + '[class*="song"]', + '[class*="chart-item"]', + '[class*="music"]', + '[data-testid*="track"]', + '[data-testid*="song"]' + ]; + + for (const selector of shazamSelectors) { + const elements = $(selector); + console.log('[shazam] Selector', selector, 'found:', elements.length, 'elements'); + + elements.each((i, el) => { + const $el = $(el); + const text = $el.text().trim(); + + if (text.length > 5) { + console.log('[shazam] Shazam element text:', text.substring(0, 100)); + + // Try different patterns for song data + const patterns = [ + /^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" + /^(\d+)\s+(.+?)\s+[-–]\s+(.+)$/, // "1 Song Title - Artist" (no period) + /^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" (with period) + /^(\d+)\s*[-–]\s*(.+?)\s*[-–]\s*(.+)$/, // "1 - Song Title - Artist" + /^(\d+)\s+(.+?)\s+by\s+(.+)$/i, // "1 Song Title by Artist" + /^(\d+)\.\s*(.+?)\s+by\s+(.+)$/i // "1. Song Title by Artist" + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const order = parseInt(match[1]); + const title = match[2].trim(); + const artist = match[3].trim(); + + if (!isNaN(order) && title && artist && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order, title, artist }); + console.log('[shazam] Found song from Shazam element:', order, title, '-', artist); + } + } + break; // Found a match, no need to try other patterns + } + } + } + }); + } + } + + // Generic song extraction as fallback + static extractSongsGeneric($, songs) { + const textContent = $.text(); + const lines = textContent.split('\n'); + + console.log('[shazam] Text content length:', textContent.length); + + for (const line of lines) { + const trimmedLine = line.trim(); + + // Try different patterns for song data + const patterns = [ + /^(\d+)\.?\s*(.+?)\s*[-–]\s*(.+)$/, // "1. Song Title - Artist" + /^(\d+)\s+(.+?)\s+(.+)$/, // "1 Song Title Artist" (no dash) + /^(\d+)\.\s*(.+?)\s*[-–]\s*(.+)$/ // "1. Song Title - Artist" (with period) + ]; + + for (const pattern of patterns) { + const match = trimmedLine.match(pattern); + if (match) { + const order = parseInt(match[1]); + const title = match[2].trim(); + const artist = match[3].trim(); + + if (!isNaN(order) && title && artist && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === title && s.artist === artist); + if (!exists) { + songs.push({ order, title, artist }); + console.log('[shazam] Found song (generic):', order, title, '-', artist); + } + } + break; // Found a match, no need to try other patterns + } + } + } + } + + // Extract songs from the specific Shazam HTML structure + static extractSongsFromShazamStructure($, songs) { + console.log('[shazam] Extracting songs from Shazam structure...'); + + // Look for the specific ul structure with li elements containing song data + $('ul').each((ulIndex, ul) => { + const $ul = $(ul); + const liElements = $ul.find('li'); + console.log('[shazam] Found ul with', liElements.length, 'li elements'); + + if (liElements.length > 0) { + liElements.each((liIndex, li) => { + const $li = $(li); + + // Look for the specific Shazam structure based on the provided HTML + const rankingNumber = $li.find('.SongItem-module_rankingNumber__3oDWK').text().trim(); + + // Find song title - try multiple approaches + let songTitle = ''; + const songTitleLink = $li.find('a[data-test-id="charts_userevent_list_songTitle"]'); + if (songTitleLink.length > 0) { + songTitle = songTitleLink.text().trim() || songTitleLink.attr('aria-label') || ''; + } + + // If no song title found, try alternative selectors + if (!songTitle) { + const titleSelectors = [ + 'a[aria-label]', + '.SongItem-module_ellipisLink__DsCMc a', + 'a.common_link__7If7r' + ]; + + for (const selector of titleSelectors) { + const titleElement = $li.find(selector); + if (titleElement.length > 0) { + const ariaLabel = titleElement.attr('aria-label'); + if (ariaLabel && ariaLabel !== rankingNumber) { + songTitle = ariaLabel; + break; + } + } + } + } + + // Find artist information - try multiple approaches + let artist = ''; + + // Method 1: Look for artist link with specific data-test-id + const artistLink = $li.find('a[data-test-id="charts_userevent_list_artistName"]'); + if (artistLink.length > 0) { + artist = artistLink.text().trim() || artistLink.attr('aria-label') || ''; + } + + // Method 2: Look for artist in the metadata line + if (!artist) { + const metadataLine = $li.find('.SongItem-module_metadataLine__7Mm6B'); + if (metadataLine.length > 0) { + // Look for any text that's not the song title + const metadataText = metadataLine.text().trim(); + if (metadataText && metadataText !== songTitle) { + artist = metadataText; + } + } + } + + // Method 3: Look for artist in the main items container + if (!artist) { + const mainContainer = $li.find('.SongItem-module_mainItemsContainer__9MRor'); + if (mainContainer.length > 0) { + // Find all text elements and look for artist + const textElements = mainContainer.find('a, span'); + for (let i = 0; i < textElements.length; i++) { + const element = $(textElements[i]); + const text = element.text().trim(); + if (text && text !== songTitle && text !== rankingNumber && !text.match(/^\d+$/)) { + artist = text; + break; + } + } + } + } + + // Method 4: Extract from image alt text + if (!artist) { + const img = $li.find('img'); + if (img.length > 0) { + const altText = img.attr('alt') || ''; + // Extract artist from alt text like "Listen to Song Title by Artist" + const altMatch = altText.match(/by (.+)$/); + if (altMatch) { + artist = altMatch[1].trim(); + } + } + } + + // Method 5: Extract from any text content as fallback + if (!artist) { + const liText = $li.text().trim(); + // Try to find artist after the song title + if (songTitle && liText.includes(songTitle)) { + const afterTitle = liText.substring(liText.indexOf(songTitle) + songTitle.length).trim(); + if (afterTitle) { + // Remove common prefixes and clean up + artist = afterTitle.replace(/^[-–\s]+/, '').trim(); + // Remove any remaining numbers or ranking info + artist = artist.replace(/\d+$/, '').trim(); + } + } + } + + console.log('[shazam] Parsed song data:', { + rankingNumber, + songTitle, + artist, + liText: $li.text().trim().substring(0, 100) + }); + + // Validate and add the song + if (rankingNumber && songTitle && artist) { + const order = parseInt(rankingNumber); + if (!isNaN(order) && order <= 200) { + // Clean up artist name (remove any remaining ranking numbers) + artist = artist.replace(/\d+$/, '').trim(); + + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === songTitle && s.artist === artist); + if (!exists) { + songs.push({ order, title: songTitle, artist }); + console.log('[shazam] Found song from Shazam structure:', order, songTitle, '-', artist); + } + } + } + }); + } + }); + + console.log('[shazam] Extracted', songs.length, 'songs from Shazam structure'); + } + + // Extract songs from the exact HTML structure provided + static extractSongsFromExactStructure($, songs) { + console.log('[shazam] Extracting songs from exact HTML structure...'); + + // Look for li elements with the specific class structure + $('li').each((liIndex, li) => { + const $li = $(li); + + // Check if this li contains the song item structure + const songItem = $li.find('.page_songItem__lAdHy'); + if (songItem.length === 0) return; + + // Extract ranking number from the specific class + const rankingElement = $li.find('.SongItem-module_rankingNumber__3oDWK'); + const rankingNumber = rankingElement.text().trim(); + + // Extract song title from the aria-label attribute + const songTitleElement = $li.find('a[data-test-id="charts_userevent_list_songTitle"]'); + let songTitle = ''; + if (songTitleElement.length > 0) { + songTitle = songTitleElement.attr('aria-label') || songTitleElement.text().trim(); + } + + // Extract artist from the artist link + const artistElement = $li.find('a[data-test-id="charts_userevent_list_artistName"]'); + let artist = ''; + if (artistElement.length > 0) { + artist = artistElement.attr('aria-label') || artistElement.text().trim(); + } + + // If we have all the data, add the song + if (rankingNumber && songTitle && artist) { + const order = parseInt(rankingNumber); + if (!isNaN(order) && order <= 200) { + // Check if we already have this song + const exists = songs.some(s => s.order === order && s.title === songTitle && s.artist === artist); + if (!exists) { + songs.push({ order, title: songTitle, artist }); + console.log('[shazam] Found song from exact structure:', order, songTitle, '-', artist); + } + } + } + }); + + console.log('[shazam] Extracted', songs.length, 'songs from exact structure'); + } + + // Get yearly top songs (for now, just return current chart data) + static async getYearlyTopSongs(year, chartType = 'top-200') { + console.log('[shazam] Getting yearly top songs for', year, 'chart type:', chartType); + + // For now, return current chart data since Shazam doesn't provide historical data + const currentData = await this.getChartData(chartType); + + // Add appearances count (all set to 1 for current data) + return currentData.map(song => ({ + ...song, + appearances: 1 + })); + } + + // Get available chart types for the API + static async getAvailableDates(year) { + // Shazam doesn't provide historical date lists + // Return current date as "today" + return [{ + date: 'today', + formattedDate: 'Today' + }]; + } +} + +module.exports = ShazamService; \ No newline at end of file diff --git a/backend/src/routes/chartRoutes.js b/backend/src/routes/chartRoutes.js index d3ce73b..1bc5322 100644 --- a/backend/src/routes/chartRoutes.js +++ b/backend/src/routes/chartRoutes.js @@ -12,4 +12,7 @@ router.get('/data/:date', chartController.getChartData); // Get yearly top songs router.get('/yearly-top/:year', chartController.getYearlyTopSongs); +// Get available Shazam chart types +router.get('/shazam/chart-types', chartController.getShazamChartTypes); + module.exports = router; \ No newline at end of file diff --git a/frontend/build/asset-manifest.json b/frontend/build/asset-manifest.json new file mode 100644 index 0000000..5ec4d9c --- /dev/null +++ b/frontend/build/asset-manifest.json @@ -0,0 +1,13 @@ +{ + "files": { + "main.css": "/static/css/main.6452a0d2.css", + "main.js": "/static/js/main.fe2339b5.js", + "index.html": "/index.html", + "main.6452a0d2.css.map": "/static/css/main.6452a0d2.css.map", + "main.fe2339b5.js.map": "/static/js/main.fe2339b5.js.map" + }, + "entrypoints": [ + "static/css/main.6452a0d2.css", + "static/js/main.fe2339b5.js" + ] +} \ No newline at end of file diff --git a/frontend/build/index.html b/frontend/build/index.html new file mode 100644 index 0000000..cd8fddc --- /dev/null +++ b/frontend/build/index.html @@ -0,0 +1 @@ +Chart Data Visualization
\ No newline at end of file diff --git a/frontend/build/static/css/main.6452a0d2.css b/frontend/build/static/css/main.6452a0d2.css new file mode 100644 index 0000000..972e722 --- /dev/null +++ b/frontend/build/static/css/main.6452a0d2.css @@ -0,0 +1,2 @@ +*{box-sizing:border-box;margin:0;padding:0}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#f5f5f5;color:#333;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.layout{background:linear-gradient(135deg,#667eea,#764ba2);min-height:100vh}.layout-container{margin:0 auto;max-width:1200px;padding:20px}.app{background:#fff;border-radius:12px;box-shadow:0 10px 30px #0000001a;overflow:hidden}.app-header{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:30px;text-align:center}.app-header h1{font-size:2.5rem;font-weight:300;margin:0}.app-main{padding:30px}.controls-section{background:#f8f9fa;border-radius:8px;display:flex;flex-direction:column;gap:20px;margin-bottom:30px;padding:20px}.source-selector{display:flex;flex-direction:column;gap:15px}.source-tabs{border-bottom:2px solid #e9ecef;display:flex;gap:10px;padding-bottom:10px}.source-tab{align-items:center;background:#0000;border:none;border-radius:8px 8px 0 0;cursor:pointer;display:flex;flex-direction:column;min-width:150px;padding:15px 20px;transition:all .2s ease}.source-tab:hover{background:#e9ecef}.source-tab.active{background:#667eea;color:#fff}.source-name{font-size:14px;font-weight:600;margin-bottom:4px}.source-description{font-size:12px;opacity:.8;text-align:center}.chart-type-selector{align-items:center;background:#fff;border:1px solid #e9ecef;border-radius:6px;display:flex;gap:10px;padding:10px}.chart-type-selector label{color:#495057;font-weight:600;white-space:nowrap}.chart-type-selector select{background:#fff;border:1px solid #e9ecef;border-radius:4px;font-size:14px;min-width:150px;padding:8px 12px}.chart-type-selector select:focus{border-color:#667eea;outline:none}.year-selector-container{grid-gap:20px;align-items:start;display:grid;gap:20px;grid-template-columns:1fr 1fr}.year-selector{display:flex;flex-direction:column;gap:10px}.year-selector label{color:#495057;font-weight:600}.year-select{background:#fff;border:2px solid #e9ecef;border-radius:6px;font-size:16px;padding:12px;transition:border-color .2s ease}.year-select:focus{border-color:#667eea;outline:none}.view-mode-selector{display:flex;flex-direction:column;gap:10px}.view-mode-selector label{color:#495057;font-weight:600}.view-mode-buttons{display:flex;gap:10px}.view-mode-btn{background:#fff;border:2px solid #e9ecef;border-radius:6px;color:#495057;cursor:pointer;flex:1 1;font-weight:600;padding:10px 16px;transition:all .2s ease}.view-mode-btn:hover{border-color:#667eea;color:#667eea}.view-mode-btn.active{background:#667eea;border-color:#667eea;color:#fff}.date-list h3{color:#495057;font-size:18px;margin-bottom:15px}.date-grid{grid-gap:10px;display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));margin-bottom:20px}.date-item{background:#fff;border:2px solid #e9ecef;border-radius:6px;cursor:pointer;font-size:14px;padding:10px;text-align:center;transition:all .2s ease}.date-item:hover{border-color:#667eea;color:#667eea}.date-item.selected{background:#667eea;border-color:#667eea;color:#fff}.chart-section{background:#fff;border-radius:8px;box-shadow:0 2px 10px #0000001a;overflow:hidden}.chart-table h3{background:#f8f9fa;border-bottom:1px solid #dee2e6;color:#495057;margin:0;padding:20px}.table-container{overflow-x:auto;padding:20px}.data-table{border-collapse:collapse;font-size:14px;width:100%}.data-table td,.data-table th{border-bottom:1px solid #dee2e6;padding:12px;text-align:left}.data-table th{color:#495057;font-weight:600;position:sticky;top:0}.data-table th,.data-table tr:hover{background:#f8f9fa}.rank-cell{color:#333;font-weight:700;width:60px}.title-cell{color:#333;font-weight:600}.artist-cell{color:#666;font-style:italic}.points-cell{color:#28a745}.points-cell,.position-cell{font-weight:600;text-align:center}.position-cell{color:#007bff}.appearances-cell{color:#6f42c1;font-weight:500;text-align:center}.data-table tr:first-child .rank-cell{color:gold;font-weight:700}.data-table tr:nth-child(2) .rank-cell{color:silver;font-weight:700}.data-table tr:nth-child(3) .rank-cell{color:#cd7f32;font-weight:700}.download-section{margin-top:20px;text-align:center}.download-button{background:linear-gradient(135deg,#28a745,#20c997);border:none;border-radius:6px;box-shadow:0 4px 15px #28a7454d;color:#fff;cursor:pointer;font-size:16px;font-weight:600;padding:12px 24px;transition:all .2s ease}.download-button:hover:not(:disabled){box-shadow:0 6px 20px #28a74566;transform:translateY(-2px)}.download-button:disabled{background:#6c757d;box-shadow:none;cursor:not-allowed}.loading{color:#6c757d;font-style:italic;padding:20px;text-align:center}.error{background:#f8d7da;border:1px solid #f5c6cb;color:#dc3545;margin:10px 0}.error,.no-data,.no-dates{border-radius:6px;padding:20px;text-align:center}.no-data,.no-dates{background:#f8f9fa;color:#6c757d;font-style:italic}.json-viewer{margin-top:20px}.json-toggle-btn{background:linear-gradient(135deg,#6c757d,#495057);border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:14px;font-weight:600;margin-bottom:10px;padding:10px 20px;transition:all .2s ease}.json-toggle-btn:hover{box-shadow:0 4px 12px #6c757d4d;transform:translateY(-1px)}.json-container{background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;margin-top:10px;overflow:hidden}.json-header{align-items:center;background:#e9ecef;border-bottom:1px solid #dee2e6;display:flex;justify-content:space-between;padding:15px 20px}.json-header h4{color:#495057;font-size:16px;margin:0}.property-selector{background:#f1f3f4;border-bottom:1px solid #dee2e6;padding:15px 20px}.property-selector h5{color:#495057;font-size:14px;font-weight:600;margin:0 0 10px}.property-checkboxes{display:flex;flex-wrap:wrap;gap:15px}.property-checkbox{align-items:center;color:#495057;cursor:pointer;display:flex;font-size:13px;gap:6px}.property-checkbox input[type=checkbox]{cursor:pointer;height:16px;width:16px}.property-label{font-weight:500;text-transform:capitalize}.json-actions{display:flex;gap:10px}.copy-btn,.download-btn{border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600;padding:8px 16px;transition:all .2s ease}.copy-btn{background:#007bff;color:#fff}.copy-btn:hover{background:#0056b3}.download-btn{background:#28a745;color:#fff}.download-btn:hover{background:#1e7e34}.json-content{word-wrap:break-word;background:#2d3748;color:#e2e8f0;font-family:Monaco,Menlo,Ubuntu Mono,monospace;font-size:13px;line-height:1.5;margin:0;max-height:400px;overflow-x:auto;overflow-y:auto;padding:20px;white-space:pre-wrap}@media (max-width:768px){.layout-container{padding:10px}.app-header{padding:20px}.app-header h1{font-size:2rem}.app-main{padding:20px}.controls-section{gap:15px}.source-tabs{flex-direction:column;gap:5px}.source-tab{border-radius:6px;min-width:auto}.year-selector-container{grid-template-columns:1fr}.date-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.data-table td,.data-table th{font-size:12px;padding:8px}} +/*# sourceMappingURL=main.6452a0d2.css.map*/ \ No newline at end of file diff --git a/frontend/build/static/css/main.6452a0d2.css.map b/frontend/build/static/css/main.6452a0d2.css.map new file mode 100644 index 0000000..3485db4 --- /dev/null +++ b/frontend/build/static/css/main.6452a0d2.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.6452a0d2.css","mappings":"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","sources":["styles.css"],"sourcesContent":["/* Reset and base styles */\r\n* {\r\n margin: 0;\r\n padding: 0;\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n background-color: #f5f5f5;\r\n color: #333;\r\n}\r\n\r\n/* Layout styles */\r\n.layout {\r\n min-height: 100vh;\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n}\r\n\r\n.layout-container {\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n padding: 20px;\r\n}\r\n\r\n/* App styles */\r\n.app {\r\n background: white;\r\n border-radius: 12px;\r\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);\r\n overflow: hidden;\r\n}\r\n\r\n.app-header {\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n color: white;\r\n padding: 30px;\r\n text-align: center;\r\n}\r\n\r\n.app-header h1 {\r\n font-size: 2.5rem;\r\n font-weight: 300;\r\n margin: 0;\r\n}\r\n\r\n.app-main {\r\n padding: 30px;\r\n}\r\n\r\n/* Controls section */\r\n.controls-section {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n margin-bottom: 30px;\r\n background: #f8f9fa;\r\n padding: 20px;\r\n border-radius: 8px;\r\n}\r\n\r\n/* Source selector */\r\n.source-selector {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 15px;\r\n}\r\n\r\n.source-tabs {\r\n display: flex;\r\n gap: 10px;\r\n border-bottom: 2px solid #e9ecef;\r\n padding-bottom: 10px;\r\n}\r\n\r\n.source-tab {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n padding: 15px 20px;\r\n border: none;\r\n background: transparent;\r\n border-radius: 8px 8px 0 0;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n min-width: 150px;\r\n}\r\n\r\n.source-tab:hover {\r\n background: #e9ecef;\r\n}\r\n\r\n.source-tab.active {\r\n background: #667eea;\r\n color: white;\r\n}\r\n\r\n.source-name {\r\n font-weight: 600;\r\n font-size: 14px;\r\n margin-bottom: 4px;\r\n}\r\n\r\n.source-description {\r\n font-size: 12px;\r\n opacity: 0.8;\r\n text-align: center;\r\n}\r\n\r\n.chart-type-selector {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n padding: 10px;\r\n background: white;\r\n border-radius: 6px;\r\n border: 1px solid #e9ecef;\r\n}\r\n\r\n.chart-type-selector label {\r\n font-weight: 600;\r\n color: #495057;\r\n white-space: nowrap;\r\n}\r\n\r\n.chart-type-selector select {\r\n padding: 8px 12px;\r\n border: 1px solid #e9ecef;\r\n border-radius: 4px;\r\n font-size: 14px;\r\n background: white;\r\n min-width: 150px;\r\n}\r\n\r\n.chart-type-selector select:focus {\r\n outline: none;\r\n border-color: #667eea;\r\n}\r\n\r\n/* Year selector and view mode */\r\n.year-selector-container {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n gap: 20px;\r\n align-items: start;\r\n}\r\n\r\n.year-selector {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n}\r\n\r\n.year-selector label {\r\n font-weight: 600;\r\n color: #495057;\r\n}\r\n\r\n.year-select {\r\n padding: 12px;\r\n border: 2px solid #e9ecef;\r\n border-radius: 6px;\r\n font-size: 16px;\r\n background: white;\r\n transition: border-color 0.2s ease;\r\n}\r\n\r\n.year-select:focus {\r\n outline: none;\r\n border-color: #667eea;\r\n}\r\n\r\n/* View mode selector */\r\n.view-mode-selector {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n}\r\n\r\n.view-mode-selector label {\r\n font-weight: 600;\r\n color: #495057;\r\n}\r\n\r\n.view-mode-buttons {\r\n display: flex;\r\n gap: 10px;\r\n}\r\n\r\n.view-mode-btn {\r\n flex: 1;\r\n padding: 10px 16px;\r\n border: 2px solid #e9ecef;\r\n border-radius: 6px;\r\n background: white;\r\n color: #495057;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n}\r\n\r\n.view-mode-btn:hover {\r\n border-color: #667eea;\r\n color: #667eea;\r\n}\r\n\r\n.view-mode-btn.active {\r\n background: #667eea;\r\n border-color: #667eea;\r\n color: white;\r\n}\r\n\r\n/* Date list */\r\n.date-list h3 {\r\n margin-bottom: 15px;\r\n color: #495057;\r\n font-size: 18px;\r\n}\r\n\r\n.date-grid {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\r\n gap: 10px;\r\n margin-bottom: 20px;\r\n}\r\n\r\n.date-item {\r\n padding: 10px;\r\n background: white;\r\n border: 2px solid #e9ecef;\r\n border-radius: 6px;\r\n text-align: center;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n font-size: 14px;\r\n}\r\n\r\n.date-item:hover {\r\n border-color: #667eea;\r\n color: #667eea;\r\n}\r\n\r\n.date-item.selected {\r\n background: #667eea;\r\n border-color: #667eea;\r\n color: white;\r\n}\r\n\r\n/* Chart section */\r\n.chart-section {\r\n background: white;\r\n border-radius: 8px;\r\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\r\n overflow: hidden;\r\n}\r\n\r\n.chart-table h3 {\r\n background: #f8f9fa;\r\n padding: 20px;\r\n margin: 0;\r\n border-bottom: 1px solid #dee2e6;\r\n color: #495057;\r\n}\r\n\r\n.table-container {\r\n overflow-x: auto;\r\n padding: 20px;\r\n}\r\n\r\n.data-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n font-size: 14px;\r\n}\r\n\r\n.data-table th,\r\n.data-table td {\r\n padding: 12px;\r\n text-align: left;\r\n border-bottom: 1px solid #dee2e6;\r\n}\r\n\r\n.data-table th {\r\n background: #f8f9fa;\r\n font-weight: 600;\r\n color: #495057;\r\n position: sticky;\r\n top: 0;\r\n}\r\n\r\n.data-table tr:hover {\r\n background: #f8f9fa;\r\n}\r\n\r\n.rank-cell {\r\n font-weight: 700;\r\n color: #333;\r\n width: 60px;\r\n}\r\n\r\n.title-cell {\r\n font-weight: 600;\r\n color: #333;\r\n}\r\n\r\n.artist-cell {\r\n color: #666;\r\n font-style: italic;\r\n}\r\n\r\n.points-cell {\r\n font-weight: 600;\r\n color: #28a745;\r\n text-align: center;\r\n}\r\n\r\n.position-cell {\r\n font-weight: 600;\r\n color: #007bff;\r\n text-align: center;\r\n}\r\n\r\n.appearances-cell {\r\n font-weight: 500;\r\n color: #6f42c1;\r\n text-align: center;\r\n}\r\n\r\n.data-table tr:nth-child(1) .rank-cell {\r\n color: #ffd700;\r\n font-weight: 700;\r\n}\r\n\r\n.data-table tr:nth-child(2) .rank-cell {\r\n color: #c0c0c0;\r\n font-weight: 700;\r\n}\r\n\r\n.data-table tr:nth-child(3) .rank-cell {\r\n color: #cd7f32;\r\n font-weight: 700;\r\n}\r\n\r\n/* Download section */\r\n.download-section {\r\n margin-top: 20px;\r\n text-align: center;\r\n}\r\n\r\n.download-button {\r\n background: linear-gradient(135deg, #28a745 0%, #20c997 100%);\r\n color: white;\r\n border: none;\r\n padding: 12px 24px;\r\n border-radius: 6px;\r\n font-size: 16px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);\r\n}\r\n\r\n.download-button:hover:not(:disabled) {\r\n transform: translateY(-2px);\r\n box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);\r\n}\r\n\r\n.download-button:disabled {\r\n background: #6c757d;\r\n cursor: not-allowed;\r\n box-shadow: none;\r\n}\r\n\r\n/* Loading and error states */\r\n.loading {\r\n text-align: center;\r\n padding: 20px;\r\n color: #6c757d;\r\n font-style: italic;\r\n}\r\n\r\n.error {\r\n text-align: center;\r\n padding: 20px;\r\n color: #dc3545;\r\n background: #f8d7da;\r\n border: 1px solid #f5c6cb;\r\n border-radius: 6px;\r\n margin: 10px 0;\r\n}\r\n\r\n.no-data,\r\n.no-dates {\r\n text-align: center;\r\n padding: 20px;\r\n color: #6c757d;\r\n font-style: italic;\r\n background: #f8f9fa;\r\n border-radius: 6px;\r\n}\r\n\r\n/* JSON Viewer styles */\r\n.json-viewer {\r\n margin-top: 20px;\r\n}\r\n\r\n.json-toggle-btn {\r\n background: linear-gradient(135deg, #6c757d 0%, #495057 100%);\r\n color: white;\r\n border: none;\r\n padding: 10px 20px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.json-toggle-btn:hover {\r\n transform: translateY(-1px);\r\n box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);\r\n}\r\n\r\n.json-container {\r\n background: #f8f9fa;\r\n border: 1px solid #dee2e6;\r\n border-radius: 8px;\r\n overflow: hidden;\r\n margin-top: 10px;\r\n}\r\n\r\n.json-header {\r\n background: #e9ecef;\r\n padding: 15px 20px;\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n border-bottom: 1px solid #dee2e6;\r\n}\r\n\r\n.json-header h4 {\r\n margin: 0;\r\n color: #495057;\r\n font-size: 16px;\r\n}\r\n\r\n.property-selector {\r\n background: #f1f3f4;\r\n padding: 15px 20px;\r\n border-bottom: 1px solid #dee2e6;\r\n}\r\n\r\n.property-selector h5 {\r\n margin: 0 0 10px 0;\r\n color: #495057;\r\n font-size: 14px;\r\n font-weight: 600;\r\n}\r\n\r\n.property-checkboxes {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 15px;\r\n}\r\n\r\n.property-checkbox {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n color: #495057;\r\n}\r\n\r\n.property-checkbox input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n}\r\n\r\n.property-label {\r\n font-weight: 500;\r\n text-transform: capitalize;\r\n}\r\n\r\n.json-actions {\r\n display: flex;\r\n gap: 10px;\r\n}\r\n\r\n.copy-btn, .download-btn {\r\n padding: 8px 16px;\r\n border: none;\r\n border-radius: 4px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n}\r\n\r\n.copy-btn {\r\n background: #007bff;\r\n color: white;\r\n}\r\n\r\n.copy-btn:hover {\r\n background: #0056b3;\r\n}\r\n\r\n.download-btn {\r\n background: #28a745;\r\n color: white;\r\n}\r\n\r\n.download-btn:hover {\r\n background: #1e7e34;\r\n}\r\n\r\n.json-content {\r\n background: #2d3748;\r\n color: #e2e8f0;\r\n padding: 20px;\r\n margin: 0;\r\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\r\n font-size: 13px;\r\n line-height: 1.5;\r\n overflow-x: auto;\r\n white-space: pre-wrap;\r\n word-wrap: break-word;\r\n max-height: 400px;\r\n overflow-y: auto;\r\n}\r\n\r\n/* Responsive design */\r\n@media (max-width: 768px) {\r\n .layout-container {\r\n padding: 10px;\r\n }\r\n \r\n .app-header {\r\n padding: 20px;\r\n }\r\n \r\n .app-header h1 {\r\n font-size: 2rem;\r\n }\r\n \r\n .app-main {\r\n padding: 20px;\r\n }\r\n \r\n .controls-section {\r\n gap: 15px;\r\n }\r\n\r\n .source-tabs {\r\n flex-direction: column;\r\n gap: 5px;\r\n }\r\n\r\n .source-tab {\r\n min-width: auto;\r\n border-radius: 6px;\r\n }\r\n\r\n .year-selector-container {\r\n grid-template-columns: 1fr;\r\n }\r\n \r\n .date-grid {\r\n grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\r\n }\r\n \r\n .data-table th,\r\n .data-table td {\r\n padding: 8px;\r\n font-size: 12px;\r\n }\r\n} "],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/frontend/build/static/js/main.fe2339b5.js b/frontend/build/static/js/main.fe2339b5.js new file mode 100644 index 0000000..43aff2e --- /dev/null +++ b/frontend/build/static/js/main.fe2339b5.js @@ -0,0 +1,3 @@ +/*! For license information please see main.fe2339b5.js.LICENSE.txt */ +(()=>{"use strict";var e={43:(e,t,n)=>{e.exports=n(202)},153:(e,t,n)=>{var r=n(43),a=Symbol.for("react.element"),l=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,u={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,n){var r,l={},s=null,c=null;for(r in void 0!==n&&(s=""+n),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(c=t.ref),t)o.call(t,r)&&!u.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===l[r]&&(l[r]=t[r]);return{$$typeof:a,type:e,key:s,ref:c,props:l,_owner:i.current}}t.Fragment=l,t.jsx=s,t.jsxs=s},202:(e,t)=>{var n=Symbol.for("react.element"),r=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),l=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),i=Symbol.for("react.provider"),u=Symbol.for("react.context"),s=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),f=Symbol.for("react.lazy"),p=Symbol.iterator;var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},m=Object.assign,y={};function g(e,t,n){this.props=e,this.context=t,this.refs=y,this.updater=n||h}function v(){}function b(e,t,n){this.props=e,this.context=t,this.refs=y,this.updater=n||h}g.prototype.isReactComponent={},g.prototype.setState=function(e,t){if("object"!==typeof e&&"function"!==typeof e&&null!=e)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},g.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},v.prototype=g.prototype;var w=b.prototype=new v;w.constructor=b,m(w,g.prototype),w.isPureReactComponent=!0;var S=Array.isArray,k=Object.prototype.hasOwnProperty,x={current:null},E={key:!0,ref:!0,__self:!0,__source:!0};function C(e,t,r){var a,l={},o=null,i=null;if(null!=t)for(a in void 0!==t.ref&&(i=t.ref),void 0!==t.key&&(o=""+t.key),t)k.call(t,a)&&!E.hasOwnProperty(a)&&(l[a]=t[a]);var u=arguments.length-2;if(1===u)l.children=r;else if(1{function n(e,t){var n=e.length;e.push(t);e:for(;0>>1,a=e[r];if(!(0>>1;rl(u,n))sl(c,u)?(e[r]=c,e[s]=n,r=s):(e[r]=u,e[i]=n,r=i);else{if(!(sl(c,n)))break e;e[r]=c,e[s]=n,r=s}}}return t}function l(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}if("object"===typeof performance&&"function"===typeof performance.now){var o=performance;t.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();t.unstable_now=function(){return i.now()-u}}var s=[],c=[],d=1,f=null,p=3,h=!1,m=!1,y=!1,g="function"===typeof setTimeout?setTimeout:null,v="function"===typeof clearTimeout?clearTimeout:null,b="undefined"!==typeof setImmediate?setImmediate:null;function w(e){for(var t=r(c);null!==t;){if(null===t.callback)a(c);else{if(!(t.startTime<=e))break;a(c),t.sortIndex=t.expirationTime,n(s,t)}t=r(c)}}function S(e){if(y=!1,w(e),!m)if(null!==r(s))m=!0,L(k);else{var t=r(c);null!==t&&z(S,t.startTime-e)}}function k(e,n){m=!1,y&&(y=!1,v(N),N=-1),h=!0;var l=p;try{for(w(n),f=r(s);null!==f&&(!(f.expirationTime>n)||e&&!P());){var o=f.callback;if("function"===typeof o){f.callback=null,p=f.priorityLevel;var i=o(f.expirationTime<=n);n=t.unstable_now(),"function"===typeof i?f.callback=i:f===r(s)&&a(s),w(n)}else a(s);f=r(s)}if(null!==f)var u=!0;else{var d=r(c);null!==d&&z(S,d.startTime-n),u=!1}return u}finally{f=null,p=l,h=!1}}"undefined"!==typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var x,E=!1,C=null,N=-1,_=5,T=-1;function P(){return!(t.unstable_now()-T<_)}function R(){if(null!==C){var e=t.unstable_now();T=e;var n=!0;try{n=C(!0,e)}finally{n?x():(E=!1,C=null)}}else E=!1}if("function"===typeof b)x=function(){b(R)};else if("undefined"!==typeof MessageChannel){var O=new MessageChannel,j=O.port2;O.port1.onmessage=R,x=function(){j.postMessage(null)}}else x=function(){g(R,0)};function L(e){C=e,E||(E=!0,x())}function z(e,n){N=g(function(){e(t.unstable_now())},n)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_continueExecution=function(){m||h||(m=!0,L(k))},t.unstable_forceFrameRate=function(e){0>e||125o?(e.sortIndex=l,n(c,e),null===r(s)&&e===r(c)&&(y?(v(N),N=-1):y=!0,z(S,l-o))):(e.sortIndex=i,n(s,e),m||h||(m=!0,L(k))),e},t.unstable_shouldYield=P,t.unstable_wrapCallback=function(e){var t=p;return function(){var n=p;p=t;try{return e.apply(this,arguments)}finally{p=n}}}},391:(e,t,n)=>{var r=n(950);t.createRoot=r.createRoot,t.hydrateRoot=r.hydrateRoot},579:(e,t,n)=>{e.exports=n(153)},730:(e,t,n)=>{var r=n(43),a=n(853);function l(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n