A Shiny for Python dashboard that visualises global Last.fm music trends — top artists, tracks, tags, and country breakdowns.
Live app: https://tiagoadrianunes.shinyapps.io/lastfm-global-trends/
Dataset: https://www.kaggle.com/datasets/tiagoadrianunes/last-fm-global-trends
The app reads from a pre-built DuckDB database (data/trends.db) hosted on Kaggle. On first run it downloads the database automatically — no live Last.fm API calls at runtime.
The fetch_countries.py script is used separately to rebuild the database from the Last.fm API and upload a new version to Kaggle when a refresh is needed.
- Python 3.11+
- uv
- A Kaggle account with API credentials
-
Clone the repository and install dependencies:
git clone https://github.com/TiagoAdriaNunes/lastfm-global-trends.git cd lastfm-global-trends uv sync -
Get your Kaggle API credentials:
- Go to https://www.kaggle.com/settings → API → Create New Token
- This downloads a
kaggle.jsonwith yourusernameandkey
-
Create a
.envfile with your Kaggle credentials:KAGGLE_USERNAME=your_username KAGGLE_KEY=your_api_key
-
Run the app (
data/trends.dbis downloaded automatically on first run):uv run shiny run --reload app.py
-
Open your browser at
http://127.0.0.1:8000
Before deploying, generate the requirements.txt file:
uv export --no-dev --no-hashes --no-annotate -o requirements.txt-
Install rsconnect-python:
uv add --dev rsconnect-python
-
Get your credentials at shinyapps.io → Account → Tokens → Show → Show secret
-
Configure the account (one-time setup):
rsconnect add \ --account YOUR_ACCOUNT_NAME \ --name YOUR_ACCOUNT_NAME \ --token YOUR_TOKEN \ --secret YOUR_SECRET
-
Deploy:
rsconnect deploy shiny . \ --name YOUR_ACCOUNT_NAME \ --title lastfm-global-trendsRedeploying an existing app: The free tier allows only 5 apps. If you hit the limit, target the existing app by ID to update it in place instead of creating a new one:
rsconnect deploy shiny . \ --name YOUR_ACCOUNT_NAME \ --title lastfm-global-trends \ --app-id YOUR_APP_IDTo find the app ID:
rsconnect apps list --name YOUR_ACCOUNT_NAME -
Set environment variables in the shinyapps.io dashboard:
Dashboard → your app → Settings → Environment Variables → add
KAGGLE_USERNAMEandKAGGLE_KEY.
fetch_countries.py pulls top artists, tracks, and tags from the Last.fm API for every supported country plus global charts, and writes the results into data/trends.db. You only need this to refresh the data and publish a new Kaggle dataset version.
- A Last.fm API key
- Add to your
.env:LASTFM_API_KEY=your_api_key LASTFM_API_SECRET=your_api_secret
uv run python fetch_countries.pyFetches all countries and global charts. Data already in the DB is skipped if it was fetched within the last 7 days (default) and the row count looks complete.
Logs are written to data/logs/run_<timestamp>.log and a JSON summary to data/logs/summary_<timestamp>.json.
Controls how old data must be before it is re-fetched. Defaults to 168 (7 days).
# Re-fetch anything older than 24 hours
uv run python fetch_countries.py --max-age 24
# Force full refresh of everything
uv run python fetch_countries.py --max-age 0Note: Even within the max-age window, a country is always re-fetched if the number of pages returned by the API no longer matches what is stored in the DB.
# Single country
uv run python fetch_countries.py --only "France"
# Multiple countries (global charts are skipped)
uv run python fetch_countries.py --only "France,Germany,Japan"Country names must match the display names in the app (e.g. "Côte d'Ivoire", "Türkiye"). A full list is in countries.py.
On each run, for every country the script:
- Checks
fetched_atin the DB — if older than--max-age, marks it stale - Fetches page 1 from the API (always, to get the current page count)
- Compares the API page count against the DB row count:
- Fresh + count matches → skip entirely, no write
- Fresh + count mismatch → re-fetch all pages and replace DB rows
- Stale → re-fetch all pages and replace DB rows unconditionally
# Run every Sunday at 2 AM
0 2 * * 0 cd /path/to/lastfm-global-trends && uv run python fetch_countries.py >> data/logs/cron.log 2>&1