diff --git a/.env.deploy.example b/.env.deploy.example index 3bfe83c..2f7fb64 100644 --- a/.env.deploy.example +++ b/.env.deploy.example @@ -1,4 +1,9 @@ APP_URL=https://your-app.workers.dev +SENTRY_ENVIRONMENT=production +# Set this as a Cloudflare secret on both workers instead of committing it here. +SENTRY_DSN= +# Optional trace sample rate from 0 to 1. Defaults to 0. +SENTRY_TRACES_SAMPLE_RATE= CLOUDFLARE_D1_DATABASE_ID= CLOUDFLARE_SESSION_KV_ID= TWITCH_CLIENT_ID= diff --git a/.env.example b/.env.example index 4b846a6..98052b4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,11 @@ APP_URL=http://localhost:9000 # Allowed hosts for local development - eg. your cloudflare tunnel domain VITE_ALLOWED_HOSTS= +SENTRY_ENVIRONMENT=development +# Optional. Leave unset locally to keep Sentry disabled. +SENTRY_DSN= +# Optional trace sample rate from 0 to 1. Defaults to 0. +SENTRY_TRACES_SAMPLE_RATE= TWITCH_CLIENT_ID= TWITCH_CLIENT_SECRET= # Generate with `openssl rand -hex 32` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..580cfcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,77 @@ +name: Bug report +description: Report a reproducible problem with the app or repository +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this. Please provide enough detail for someone else to reproduce the issue. + - type: textarea + id: summary + attributes: + label: Summary + description: What happened? + placeholder: A clear and concise description of the problem. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: List the exact steps needed to reproduce the problem. + placeholder: | + 1. Go to ... + 2. Click ... + 3. Run ... + 4. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen instead? + placeholder: Describe the expected result. + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead? + placeholder: Describe the actual result. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs or screenshots + description: Paste any relevant logs, stack traces, screenshots, or recordings. + render: shell + - type: input + id: version + attributes: + label: Version or commit + description: Which release, branch, or commit did you test? + placeholder: main, v0.1.0, or a commit SHA + - type: textarea + id: environment + attributes: + label: Environment + description: Share the environment where this happened. + placeholder: | + OS: + Browser: + Node version: + Deployment target: + Local or deployed: + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other useful detail. + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..572da34 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/Jamesllllllllll/request-bot/blob/main/README.md + about: Review the README and deployment docs before opening a support issue. + - name: Deployment workflow + url: https://github.com/Jamesllllllllll/request-bot/blob/main/docs/deployment-workflow.md + about: Check the deployment guide for Cloudflare setup and production configuration. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9b50334 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,38 @@ +name: Feature request +description: Suggest an improvement or new capability +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Please describe the problem you are trying to solve before proposing a solution. + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What problem are you running into? Who is affected? + placeholder: Describe the use case and why it matters. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like to happen? + placeholder: Describe the behavior, workflow, or API you want. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What alternatives have you considered, including workarounds? + placeholder: Describe any other approaches you evaluated. + - type: textarea + id: additional + attributes: + label: Additional context + description: Add links, examples, screenshots, or references if relevant. + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2214df0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## Summary + +Describe the change at a high level. + +## What Changed + +- + +## How To Test + +1. + +## Screenshots Or UI Notes + +Include screenshots, recordings, or notes for any visible UI change. + +## Checklist + +- [ ] I tested the change locally +- [ ] I updated docs or examples if needed +- [ ] I called out any follow-up work or known limitations + diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 7794e6b..b931d41 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -47,6 +47,9 @@ jobs: - name: Generate production Wrangler configs run: npm run deploy:prepare + - name: Apply production D1 migrations + run: npm run db:migrate:remote + - name: Deploy production backend shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00c0fc7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Added +- Production-ready Sentry scaffolding for the Cloudflare app and backend workers, with DSN-based opt-in for local development and Cloudflare-managed secrets for deployed environments. +- GitHub issue templates, a pull request template, and a repository `CODE_OF_CONDUCT.md`. + +### Changed +- Simplified catalog song source URLs to always derive the Ignition download link from the song source ID instead of storing `source_url` in the database. +- Added a migration to remove the redundant `catalog_songs.source_url` column and updated the sample catalog seed to match the new schema. +- Tightened schema version checks so the app only accepts migrations that are actually present in the repo. +- Expanded deployment and environment documentation for Sentry configuration in local development and production. + +## [0.1.0] - 2026-03-18 + +### Added +- Initial public release of the Twitch request bot with playlist management, dashboard controls, and Cloudflare-backed deployment support. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..af0b36c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,105 @@ +# Code of Conduct + +## TL;DR + +Be respectful, stay constructive, and keep discussion focused on the work. Harassment, personal attacks, abusive behavior, and sharing private information without permission are not acceptable. If there is a problem, contact the maintainers through GitHub rather than escalating publicly in a thread. + +## Our Commitment + +In the interest of fostering an open, professional, and respectful community, contributors and maintainers of this project commit to making participation in this project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We commit to acting and interacting in ways that contribute to an open, technically constructive, and respectful environment. + +## Our Standards + +Examples of behavior that contributes to a positive environment include: + +- Demonstrating empathy and respect toward other people +- Giving and receiving constructive feedback professionally +- Focusing discussion on the work, the code, and the documented behavior +- Being clear about assumptions, tradeoffs, and technical constraints +- Accepting responsibility for mistakes and correcting them promptly + +Examples of unacceptable behavior include: + +- Harassment, intimidation, threats, or personal attacks +- Trolling, insulting, or derogatory comments +- Public or private abuse directed at contributors, maintainers, or users +- Deliberate misrepresentation of another person’s work or intent +- Publishing someone else’s private information without explicit permission +- Disruptive behavior that repeatedly derails technical discussion or collaboration + +## Enforcement Responsibilities + +Project maintainers are responsible for clarifying and enforcing these standards and may take appropriate and fair corrective action in response to behavior they deem inappropriate, threatening, offensive, or harmful. + +Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all project spaces, including: + +- GitHub issues +- Pull requests +- Discussions tied to the repository +- Review comments +- Project documentation and contribution threads + +It also applies when an individual is officially representing the project in public spaces. + +## Reporting + +If you experience or witness behavior that violates this Code of Conduct, please contact the maintainers through GitHub rather than opening a public argument in an issue or pull request. + +For now, use one of these paths: + +- Open a GitHub Discussion if the matter can be handled publicly and constructively +- Open an issue only if the problem is about repository process rather than a personal conduct concern +- Contact a maintainer directly through GitHub if the matter should be handled more discreetly + +Maintainers will review reports and respond as reasonably as possible based on availability and the information provided. + +## Enforcement Guidelines + +Maintainers may use the following types of responses when determining consequences for conduct violations: + +### 1. Correction + +For use when behavior is inappropriate but limited in scope. + +Examples: + +- A private or public clarification +- A request to stop a specific behavior +- A reminder of project expectations + +### 2. Warning + +For use when there is a pattern of inappropriate behavior or a more serious single incident. + +Examples: + +- A formal warning +- Temporary limits on participation in discussions or reviews + +### 3. Temporary Ban + +For use when behavior causes sustained disruption or creates an unsafe environment. + +Examples: + +- Temporary suspension from issues, pull requests, or discussions +- Temporary loss of contribution privileges + +### 4. Permanent Ban + +For use when a participant demonstrates a pattern of harmful behavior or a serious violation that makes continued participation inappropriate. + +Examples: + +- Permanent removal from project spaces +- Blocking from future participation in the repository + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, with modifications for this repository’s current reporting workflow. diff --git a/README.md b/README.md index 63c9446..7ffc937 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,9 @@ Current `.env.example`: ```env APP_URL=http://localhost:9000 +SENTRY_ENVIRONMENT=development +SENTRY_DSN= +SENTRY_TRACES_SAMPLE_RATE= TWITCH_CLIENT_ID= TWITCH_CLIENT_SECRET= TWITCH_EVENTSUB_SECRET=local-dev-eventsub-secret @@ -102,6 +105,16 @@ To test Twitch sign-in, bot behavior, and EventSub locally, also set: `ADMIN_TWITCH_USER_IDS` should contain the Twitch user ID for the admin account that is allowed to connect the shared bot account and access admin pages. +Sentry stays off locally unless you explicitly set a DSN: + +- `SENTRY_DSN` + +If you want to test Sentry locally, use your own test DSN in `.env` and keep: + +- `SENTRY_ENVIRONMENT=development` + +Production should keep using the deployed Worker secret for `SENTRY_DSN`. + URL split: - `.env` is for local development @@ -129,6 +142,14 @@ That does three things: The bundled sample catalog is intended for local development and preview deployments. `db:bootstrap:local` is destructive for local D1 data by design. +For day-to-day schema changes during local development, use: + +```bash +npm run db:migrate +``` + +That applies any new checked-in Drizzle SQL migrations to your local D1 database without resetting local data. + ### 5. Start the app ```bash @@ -322,6 +343,7 @@ echo "" | npx wrangler secret put TWITCH_CLIENT_SECRET --c echo "" | npx wrangler secret put TWITCH_EVENTSUB_SECRET --config wrangler.jsonc echo "" | npx wrangler secret put SESSION_SECRET --config wrangler.jsonc echo "" | npx wrangler secret put ADMIN_TWITCH_USER_IDS --config wrangler.jsonc +echo "" | npx wrangler secret put SENTRY_DSN --config wrangler.jsonc ``` Backend Worker: @@ -330,6 +352,7 @@ Backend Worker: echo "" | npx wrangler secret put TWITCH_CLIENT_ID --config wrangler.aux.jsonc echo "" | npx wrangler secret put TWITCH_CLIENT_SECRET --config wrangler.aux.jsonc echo "" | npx wrangler secret put TWITCH_EVENTSUB_SECRET --config wrangler.aux.jsonc +echo "" | npx wrangler secret put SENTRY_DSN --config wrangler.aux.jsonc ``` If the Worker does not exist yet, `wrangler secret put` creates it before uploading the secret. @@ -345,6 +368,9 @@ Add the returned D1 and KV IDs to `.env.deploy` instead of editing the committed ```env CLOUDFLARE_D1_DATABASE_ID= CLOUDFLARE_SESSION_KV_ID= +SENTRY_ENVIRONMENT=production +# Optional +SENTRY_TRACES_SAMPLE_RATE=0.1 ``` Use: @@ -357,16 +383,37 @@ GitHub Actions does not use a checked-in `.env.deploy` file. The deploy workflow - repository secrets for Cloudflare IDs and tokens - the repository `APP_URL` secret for the deployed public URL -- repository variables for non-secret runtime values such as `TWITCH_BOT_USERNAME` and `TWITCH_SCOPES` +- repository variables for non-secret runtime values such as `TWITCH_BOT_USERNAME`, `TWITCH_SCOPES`, `SENTRY_ENVIRONMENT`, and optionally `SENTRY_TRACES_SAMPLE_RATE` The repo generates gitignored deploy configs in `.generated/` from `.env.deploy`, so your real Cloudflare IDs stay out of tracked files. +Sentry runtime notes: + +- events are sent whenever `SENTRY_DSN` is present +- local development should use your own test Sentry DSN in `.env` +- the DSN should be set as a Cloudflare Worker secret on both deployed Workers +- release tagging uses the Worker version metadata binding automatically, with `SENTRY_RELEASE` as an optional override +- D1 access is instrumented automatically when Sentry is enabled + Seed the remote D1 database with the bundled catalog: ```bash npm run db:bootstrap:remote ``` +After GitHub production deploys are enabled, normal production schema changes should go through pull requests and merge to `main`. +The production GitHub Actions workflow applies remote D1 migrations before deploying the Workers, so contributors only need to run local migrations during development. + +Remote-affecting npm scripts are guarded and exit with a helper message outside CI. +That includes: + +- `npm run db:migrate:remote` +- `npm run db:seed:sample:remote` +- `npm run db:bootstrap:remote` +- `npm run deploy` + +If you intentionally need an operator-only override for maintenance, rerun them with `ALLOW_REMOTE_OPERATIONS=1`. + Deploy with the repo script: ```bash @@ -385,6 +432,9 @@ npx wrangler d1 execute request_bot --remote --config .generated/wrangler.produc Once the repo is on GitHub, `main` can deploy automatically through the included GitHub Actions workflows. +Production deploys from GitHub Actions also apply remote D1 migrations before the backend and frontend Workers are deployed. +The remote migration script is blocked outside CI by default so normal contributor workflows stay local-only. + Before enabling GitHub deploys: 1. Create the GitHub repository. diff --git a/data/sample-catalog.sql b/data/sample-catalog.sql index 54cbb76..dda0e3a 100644 --- a/data/sample-catalog.sql +++ b/data/sample-catalog.sql @@ -2,38 +2,38 @@ DELETE FROM catalog_songs; DELETE FROM search_cache; DELETE FROM search_rate_limits; -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fe71830b96a64f9ca487a7a940ad1792', 'library', 99084, 'https://example.com/songs/99084', 'Ok, But This Is The Last Time', 'The Offspring', 'Supercharged', 'Hikikomori', '[]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:35', 215, 2024, '1.1', 105, 51, 0, 0, 104, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/c/ce/The_Offspring_-_Supercharged.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fb9bf6b9b42e46ab980a796154f96504', 'library', 99083, 'https://example.com/songs/99083', 'Whips-A-Swinging', 'Cruel Force', 'Savage Gods', 'Manch', '[]', 'Metal', 'Thrash Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc"]', '4:11', 251, 2026, '1', 31, 32, 1, 0, 31, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 'https://i.scdn.co/image/ab67616d0000b2732a93a4952dc996399ce4e67d', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_5e52d9dbbeea471ca4683636778cbc03', 'library', 99081, 'https://example.com/songs/99081', 'Neon Noir', 'VV', 'Neon Noir', 'JohnCryx', '[]', 'Rock', 'Alternative Rock', 'E Standard | A Standard', 'E Standard', 'E Standard', 'A Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:49', 229, 2023, '1', 45, 28, 0, 0, 45, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/3/3e/VV_-_Neon_Noir.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_33c60a5d561e4e519b371f1f1f636c92', 'library', 99080, 'https://example.com/songs/99080', 'Black Cat', 'David Gilmour', 'Luck and Strange', 'shotfirer', '[]', 'Rock', 'Classic Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '1:42', 102, 2024, '1', 105, 52, 3, 0, 104, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 'https://is1-ssl.mzstatic.com/image/thumb/Music221/v4/96/25/6e/96256e5f-8bb7-91fe-9f93-f58e367bee68/196873967172.jpg/1200x630bb.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_30ce256ff9a9418d966bb3d1144af256', 'library', 99079, 'https://example.com/songs/99079', 'Kyuukei Mugen Rensa ~MIZUTAMA~', 'Aicle.', 'Kyuukei Mugen Rensa~MIZUTAMA~', 'JokerTheAnarchist', '[]', 'Rock', NULL, 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '4:45', 285, 2007, '1', 16, 39, 0, 0, 16, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 'https://vk.gy/images/161468-kyuukei-mugen-rensa-mizutama-photo.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_c8f25ebdd52c4f08a2b647c023b0ae3f', 'library', 99078, 'https://example.com/songs/99078', 'On My Soul', 'Bruno Mars', 'The Romantic', 'Djpavs', '[]', 'Pop', 'Funk', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '2:54', 174, 2026, '1', 115, 43, 1, 0, 115, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273e2887a1b6b98fc6c73bab12b', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_b9a5aafd5d0d48f48624fd6098bb2a1c', 'library', 99077, 'https://example.com/songs/99077', 'Before the Throne of God Above', 'Sovereign Grace Music', 'Sovereign Grace Music Collection', 'rayfinkle', '["Kristyn Getty"]', NULL, NULL, 'Eb Standard', NULL, 'Eb Standard', 'Eb Standard', '["rhythm","bass","voice"]', '["pc","mac"]', '3:34', 214, 2000, '1', 30, 22, 0, 0, 29, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 'https://sovereigngracemusic.s3.amazonaws.com/wp-content/uploads/20230821175214/sgmcollectioncover_final-1024x1024.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6a03ae249f0b426a8770bb0881266406', 'library', 99076, 'https://example.com/songs/99076', 'Thinkin Bout You', 'Flea', 'Honora', 'shotfirer', '[]', 'Jazz', 'Fusion', 'E Standard', NULL, NULL, 'E Standard', '["bass"]', '["pc","mac"]', '4:20', 260, 2026, '1', 59, 48, 1, 0, 59, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://f4.bcbits.com/img/a0084861691_16.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6ed886eb93574e59888bb475eecd4777', 'library', 99075, 'https://example.com/songs/99075', 'FREE!', 'SEU Worship', 'Move of God', 'rayfinkle', '["David Ryan Cook"]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:46', 226, 2024, '1', 38, 14, 0, 0, 37, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://seuworship.com/wp-content/uploads/SEUWorship_MoveOfGod_CVR_WEB.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_3a6640482a584a3c877031d636f6ee0c', 'library', 99074, 'https://example.com/songs/99074', 'What A God', 'SEU Worship', 'Move of God', 'rayfinkle', '[]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '7:18', 438, 2024, '1', 40, 21, 1, 0, 40, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 'https://seuworship.com/wp-content/uploads/SEUWorship_MoveOfGod_CVR_WEB.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_70c63a69c0e2480cb6810f0efab42838', 'library', 99073, 'https://example.com/songs/99073', 'The Ones That Changed Me', 'Interloper', 'A Forgotten Loss', 'lallomes', '[]', 'Metal', 'Progressive Metal', 'B Standard (7 string) | B Standard', 'B Standard (7 string)', 'B Standard (7 string)', 'B Standard', '["lead","rhythm","bass"]', '["pc","mac"]', '4:39', 279, 2024, '1', 25, 19, 2, 0, 25, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://lastfm.freetls.fastly.net/i/u/770x0/1e83b4e1cb2229581f42e93ccb00417d.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_24c73291fdc14600838446e5bb9d5af2', 'library', 99072, 'https://example.com/songs/99072', 'Livin'' On The Edge', 'Aerosmith', 'Get A Grip', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'B Standard', NULL, NULL, 'B Standard', '["bass","voice"]', '["pc"]', '6:17', 377, 1993, '1', 155, 102, 0, 0, 153, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/9/9f/GetAGrip_Aerosmithalbum.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_27c92d1ea91845fa90effc3aa49dd1ae', 'library', 99071, 'https://example.com/songs/99071', 'Survivor''s Guilt', 'Vylet Pony', 'Monarch of Monsters', 'hinga_dinga', '[]', 'Rock', 'Punk Rock', 'E Standard', NULL, NULL, 'E Standard', '["bass","voice"]', '["pc","mac"]', '5:57', 357, 2024, '1', 19, 17, 0, 0, 19, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://cdn-images.dzcdn.net/images/cover/75ab8a932db70c90b0b1a2f60425c29d/500x500.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_8533eee0adb54000b3caa466565ab2fc', 'library', 99070, 'https://example.com/songs/99070', 'All I Want', 'Lynch Mob', 'Wicked Sensation', 'Hikikomori', '[]', NULL, NULL, 'Eb Standard', 'Eb Standard', 'Eb Standard', 'Eb Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '5:14', 314, 1990, '1.2', 64, 34, 1, 0, 63, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/c/c1/LynchMob_Wicked.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_5396e11d36b846f7baa001cc15745965', 'library', 99060, 'https://example.com/songs/99060', 'Mr. Blackwell', 'Kiss', 'Music From The Elder', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '5:02', 302, 1981, '1.1', 94, 61, 1, 0, 94, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/f/fd/The_elder_album_cover.jpg', 1773187200000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_086adcbb3c4c4951bc9b047890163705', 'library', 98548, 'https://example.com/songs/98548', 'Come Back', 'Pearl Jam', 'Pearl Jam', 'elopez', '[]', 'Rock', 'Alternative Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc","mac"]', '5:26', 326, 2006, '4', 278, 291, 3, 1, 274, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/8/8b/PearlJam1.jpg', 1769990400000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_b9135dd2d36d48e78d6c0b856dc27f64', 'library', 80543, 'https://example.com/songs/80543', 'Cheque', 'Astroid Boys', 'Broke', 'JohnCryx', '[]', 'Metal', 'Rap Metal', 'Eb Drop Db', 'Eb Drop Db', 'Eb Drop Db', 'Eb Drop Db', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:33', 213, 2017, '2.2', 77, 55, 0, 0, 75, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.discogs.com/VlNrpfIlrG7dRyEPFfqlJKH_sd0PVEkhDxt_qQfrfTE/rs:fit/g:sm/q:90/h:599/w:599/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEwOTMw/OTEyLTE1MDY3MTgz/MDEtNjMxMS5qcGVn.jpeg', 1695340800000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_20cfe2b18fe34c1b86f7afce2c934b0c', 'library', 22085, 'https://example.com/songs/22085', 'Jaded', 'Aerosmith', 'Just Push Play', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '3:45', 225, 2001, '3', 4402, 357, 5, 0, 1447, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 'https://images.rapgenius.com/b49790bfeebea5616e3b733886cc2be0.934x928x1.jpg', 1461542400000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_9e752b6ccf6d4556a202f06468af5a1e', 'library', 99069, 'https://example.com/songs/99069', 'Johnny B.', 'The Hooters', 'One Way Home', 'Manch', '[]', 'Rock', 'Classic Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc"]', '4:09', 249, 1987, '1', 60, 50, 4, 0, 60, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 'https://i.scdn.co/image/ab67616d0000b273be127745dc23ededb57dcd9b', 1773187200000, 1773187200000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_3f34c66c0acc4f94a6ab68b6dee2b508', 'library', 99068, 'https://example.com/songs/99068', 'Boku Wa Maou', 'Aicle.', 'Dokuiro Marbel', 'JokerTheAnarchist', '[]', 'Rock', NULL, 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '4:22', 262, 2007, '1', 28, 22, 0, 0, 27, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 'https://www.spirit-of-metal.com/les%20goupes/A/Aicle/Dokuiro%20Marbel/Dokuiro%20Marbel.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_27ecffd7bd8746c28b00da2a7757d04d', 'library', 99067, 'https://example.com/songs/99067', 'Typical Fashion', 'Love Lost But Not Forgotten', 'Upon the Right, I Saw a New Misery', 'jamesiety', '[]', 'Metal', 'Metalcore', 'D Drop C', 'D Drop C', NULL, 'D Drop C', '["lead","bass","voice"]', '["pc","mac"]', '1:41', 101, 2002, '1', 25, 33, 5, 0, 24, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://f4.bcbits.com/img/a2328027614_16.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_0507104fa1824cb7adb51ec3c9f89f1e', 'library', 99066, 'https://example.com/songs/99066', 'Mista Bone', 'Great White', 'Twice Shy', 'liteofspace', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '5:26', 326, 1989, '1', 76, 49, 7, 0, 74, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://m.media-amazon.com/images/I/71YxAt9i1bL._SS500_.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6a508c2c2c9a48faa4cd3864e1a60615', 'library', 99065, 'https://example.com/songs/99065', 'Where Dragons Rule', 'DragonForce', 'Valley of the Damned', 'Lord Sai', '[]', 'Metal', 'Power Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '6:01', 361, 2003, '1', 71, 40, 0, 0, 69, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/a/a9/DragonForce-ValleyOfTheDamned-AlbumCover.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_9f64eccba6e74dd7b915beac9194a590', 'library', 99064, 'https://example.com/songs/99064', 'Evening Star', 'DragonForce', 'Valley of the Damned', 'Lord Sai', '[]', 'Metal', 'Power Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '6:49', 409, 2003, '1', 61, 39, 0, 0, 60, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/a/a9/DragonForce-ValleyOfTheDamned-AlbumCover.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_294f53afe5d845f48ddc1dc5b27d1639', 'library', 99063, 'https://example.com/songs/99063', 'I''ve Been Waiting', 'Lucille Two', 'Lucille Two', 'Boltimoore', '[]', 'Rock', 'Indie Pop', 'E Standard', NULL, NULL, 'E Standard', '["bass","voice"]', '["pc","mac"]', '4:02', 242, 2024, '1', 32, 17, 1, 0, 32, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273d43d27d52a9df5ff4e5d74d2', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_ad0de0abeef146c8b5a687547cfbf5f0', 'library', 99062, 'https://example.com/songs/99062', 'Scatman (ski-ba-bop-ba-dop-bop)', 'Scatman John', 'Scatman''s World', 'Emmy-Dell', '[]', 'Pop', NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:43', 223, 1995, '1', 261, 148, 3, 0, 261, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273826ecb816b4126f849732ff0', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_78ce2a004bc541e2a8c7e9f3227ffdb4', 'library', 99061, 'https://example.com/songs/99061', 'Ravenblack', 'Edguy', 'Monuments', 'MCHBass', '[]', 'Metal', NULL, 'D Standard', NULL, NULL, 'D Standard', '["bass"]', '["pc"]', '5:07', 307, 2017, '1', 25, 22, 1, 0, 25, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 'https://i.discogs.com/OIf0Q29TGZI9QnQdAbCFp1EgRXnjEQ9FQNRB7x_9PFs/rs:fit/g:sm/q:90/h:597/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEwNTYz/MjY5LTE0OTk5NjE2/MjAtODAwNS5qcGVn.jpeg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fdbf32ab43fc453993cd68f5568902cb', 'library', 99051, 'https://example.com/songs/99051', 'All In Now', 'Dogstar', 'All In Now', 'shotfirer', '[]', 'Rock', 'Alternative Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc","mac"]', '2:56', 176, 2026, '1.1', 180, 161, 5, 0, 177, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 'https://cdn.shopify.com/s/files/1/0867/1120/6219/files/2000x2000bb_4bfd04b6-0890-458a-8b4f-372ff9460af1.jpg', 1773014400000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fa0d13c3950e4a048f6099938a42bbab', 'library', 99020, 'https://example.com/songs/99020', 'The Saw Is The Law', 'Sodom', 'Better Off Dead', 'Sinomod', '[]', 'Metal', 'Heavy Metal', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '4:23', 263, 1990, '1.1', 99, 91, 7, 0, 99, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/b/ba/Sodom-betteroffdead.jpg', 1772841600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_4a479e03e0764e7dae26a2d0ee54833a', 'library', 98954, 'https://example.com/songs/98954', 'The Prince', 'Metallica', '...And Justice For All', 'RadicalTyrant', '[]', 'Metal', NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass"]', '["pc","mac"]', '4:26', 266, 1988, '2', 276, 278, 2, 0, 272, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://i.ibb.co/fYKzJQyL/Metallica-Harvester-of-Sorrow-cover.jpg', 1772409600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_c03ef498ee9b48e7b69e09f767d88c0d', 'library', 95440, 'https://example.com/songs/95440', 'Another Brick In The Wall Pt. 2', 'Pink Floyd', 'The Wall', 'Emmy-Dell', '[]', 'Rock', 'Classic Rock', 'E Standard | Drop D', 'E Standard', 'E Standard', 'Drop D', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '4:12', 252, 1979, '1.1', 2161, 1198, 5, 0, 2132, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273cbf8b2cd89b54a021db51e75', 1753833600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_aef3bbafbb29476d9cf7a318ca71693c', 'library', 94829, 'https://example.com/songs/94829', 'Piano Lessons', 'Porcupine Tree', 'Stupid Dream', 'shiraishi2012', '[]', 'Rock', 'Progressive Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc"]', '4:31', 271, 1999, '1.4', 249, 147, 1, 0, 248, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://lastfm.freetls.fastly.net/i/u/500x500/1e48bfd86d204cbcad9b3662debfd9f0.jpg', 1750550400000, 1773187200000, 1773439712094, 1773509466992, 1773439712094, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_e0caf472dd0749499c1345e92b615dab', 'library', 77263, 'https://example.com/songs/77263', 'Claw The Clouds', 'Satariel', 'Hydra', 'squirrelperson', '[]', 'Metal', 'Melodic Death Metal', 'Custom Tuning', 'Custom Tuning', 'Custom Tuning', NULL, '["lead","rhythm","voice"]', '["pc","mac"]', '3:58', 238, 2005, '2', 63, 21, 2, 0, 59, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://www.metal-archives.com/images/7/7/1/2/77121.jpg', 1683590400000, 1773187200000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_2fee2eedcd5f4dc68e4d2a1962d04d0e', 'library', 76612, 'https://example.com/songs/76612', 'Otsego Amigo', 'Static-X', 'Start a War', 'Mage_Gage', '[]', 'Metal', 'Industrial', 'C Drop Bb', 'C Drop Bb', 'C Drop Bb', 'C Drop Bb', '["lead","rhythm","bass","voice"]', '["pc"]', '2:45', 165, 2005, '1.4', 277, 80, 1, 0, 251, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/a/a4/Start_a_war.jpg', 1681344000000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); -INSERT INTO catalog_songs (id, source, source_song_id, source_url, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_e3c4da0f7af849f190220f3239933717', 'library', 58021, 'https://example.com/songs/58021', 'Line In The Sand (Evolution Theme)', 'Jim Johnston', 'WWE: The Music, Vol. 6', 'Emmy-Dell', '["Motörhead"]', 'Rock', 'Hard Rock', 'Eb Standard', 'Eb Standard', 'Eb Standard', 'Eb Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:44', 224, 2004, '2', 683, 118, 0, 0, 658, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b27325d6ca20037122522b1cb8a4', 1617062400000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fe71830b96a64f9ca487a7a940ad1792', 'library', 99084, 'Ok, But This Is The Last Time', 'The Offspring', 'Supercharged', 'Hikikomori', '[]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:35', 215, 2024, '1.1', 105, 51, 0, 0, 104, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/c/ce/The_Offspring_-_Supercharged.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fb9bf6b9b42e46ab980a796154f96504', 'library', 99083, 'Whips-A-Swinging', 'Cruel Force', 'Savage Gods', 'Manch', '[]', 'Metal', 'Thrash Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc"]', '4:11', 251, 2026, '1', 31, 32, 1, 0, 31, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 'https://i.scdn.co/image/ab67616d0000b2732a93a4952dc996399ce4e67d', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_5e52d9dbbeea471ca4683636778cbc03', 'library', 99081, 'Neon Noir', 'VV', 'Neon Noir', 'JohnCryx', '[]', 'Rock', 'Alternative Rock', 'E Standard | A Standard', 'E Standard', 'E Standard', 'A Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:49', 229, 2023, '1', 45, 28, 0, 0, 45, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/3/3e/VV_-_Neon_Noir.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_33c60a5d561e4e519b371f1f1f636c92', 'library', 99080, 'Black Cat', 'David Gilmour', 'Luck and Strange', 'shotfirer', '[]', 'Rock', 'Classic Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '1:42', 102, 2024, '1', 105, 52, 3, 0, 104, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 'https://is1-ssl.mzstatic.com/image/thumb/Music221/v4/96/25/6e/96256e5f-8bb7-91fe-9f93-f58e367bee68/196873967172.jpg/1200x630bb.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_30ce256ff9a9418d966bb3d1144af256', 'library', 99079, 'Kyuukei Mugen Rensa ~MIZUTAMA~', 'Aicle.', 'Kyuukei Mugen Rensa~MIZUTAMA~', 'JokerTheAnarchist', '[]', 'Rock', NULL, 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '4:45', 285, 2007, '1', 16, 39, 0, 0, 16, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 'https://vk.gy/images/161468-kyuukei-mugen-rensa-mizutama-photo.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_c8f25ebdd52c4f08a2b647c023b0ae3f', 'library', 99078, 'On My Soul', 'Bruno Mars', 'The Romantic', 'Djpavs', '[]', 'Pop', 'Funk', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '2:54', 174, 2026, '1', 115, 43, 1, 0, 115, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273e2887a1b6b98fc6c73bab12b', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_b9a5aafd5d0d48f48624fd6098bb2a1c', 'library', 99077, 'Before the Throne of God Above', 'Sovereign Grace Music', 'Sovereign Grace Music Collection', 'rayfinkle', '["Kristyn Getty"]', NULL, NULL, 'Eb Standard', NULL, 'Eb Standard', 'Eb Standard', '["rhythm","bass","voice"]', '["pc","mac"]', '3:34', 214, 2000, '1', 30, 22, 0, 0, 29, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 'https://sovereigngracemusic.s3.amazonaws.com/wp-content/uploads/20230821175214/sgmcollectioncover_final-1024x1024.png', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6a03ae249f0b426a8770bb0881266406', 'library', 99076, 'Thinkin Bout You', 'Flea', 'Honora', 'shotfirer', '[]', 'Jazz', 'Fusion', 'E Standard', NULL, NULL, 'E Standard', '["bass"]', '["pc","mac"]', '4:20', 260, 2026, '1', 59, 48, 1, 0, 59, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://f4.bcbits.com/img/a0084861691_16.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6ed886eb93574e59888bb475eecd4777', 'library', 99075, 'FREE!', 'SEU Worship', 'Move of God', 'rayfinkle', '["David Ryan Cook"]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:46', 226, 2024, '1', 38, 14, 0, 0, 37, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://seuworship.com/wp-content/uploads/SEUWorship_MoveOfGod_CVR_WEB.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_3a6640482a584a3c877031d636f6ee0c', 'library', 99074, 'What A God', 'SEU Worship', 'Move of God', 'rayfinkle', '[]', NULL, NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '7:18', 438, 2024, '1', 40, 21, 1, 0, 40, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 'https://seuworship.com/wp-content/uploads/SEUWorship_MoveOfGod_CVR_WEB.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_70c63a69c0e2480cb6810f0efab42838', 'library', 99073, 'The Ones That Changed Me', 'Interloper', 'A Forgotten Loss', 'lallomes', '[]', 'Metal', 'Progressive Metal', 'B Standard (7 string) | B Standard', 'B Standard (7 string)', 'B Standard (7 string)', 'B Standard', '["lead","rhythm","bass"]', '["pc","mac"]', '4:39', 279, 2024, '1', 25, 19, 2, 0, 25, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://lastfm.freetls.fastly.net/i/u/770x0/1e83b4e1cb2229581f42e93ccb00417d.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_24c73291fdc14600838446e5bb9d5af2', 'library', 99072, 'Livin'' On The Edge', 'Aerosmith', 'Get A Grip', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'B Standard', NULL, NULL, 'B Standard', '["bass","voice"]', '["pc"]', '6:17', 377, 1993, '1', 155, 102, 0, 0, 153, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/9/9f/GetAGrip_Aerosmithalbum.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_27c92d1ea91845fa90effc3aa49dd1ae', 'library', 99071, 'Survivor''s Guilt', 'Vylet Pony', 'Monarch of Monsters', 'hinga_dinga', '[]', 'Rock', 'Punk Rock', 'E Standard', NULL, NULL, 'E Standard', '["bass","voice"]', '["pc","mac"]', '5:57', 357, 2024, '1', 19, 17, 0, 0, 19, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://cdn-images.dzcdn.net/images/cover/75ab8a932db70c90b0b1a2f60425c29d/500x500.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_8533eee0adb54000b3caa466565ab2fc', 'library', 99070, 'All I Want', 'Lynch Mob', 'Wicked Sensation', 'Hikikomori', '[]', NULL, NULL, 'Eb Standard', 'Eb Standard', 'Eb Standard', 'Eb Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '5:14', 314, 1990, '1.2', 64, 34, 1, 0, 63, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/c/c1/LynchMob_Wicked.jpg', 1773273600000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_5396e11d36b846f7baa001cc15745965', 'library', 99060, 'Mr. Blackwell', 'Kiss', 'Music From The Elder', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '5:02', 302, 1981, '1.1', 94, 61, 1, 0, 94, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/f/fd/The_elder_album_cover.jpg', 1773187200000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_086adcbb3c4c4951bc9b047890163705', 'library', 98548, 'Come Back', 'Pearl Jam', 'Pearl Jam', 'elopez', '[]', 'Rock', 'Alternative Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc","mac"]', '5:26', 326, 2006, '4', 278, 291, 3, 1, 274, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/8/8b/PearlJam1.jpg', 1769990400000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_b9135dd2d36d48e78d6c0b856dc27f64', 'library', 80543, 'Cheque', 'Astroid Boys', 'Broke', 'JohnCryx', '[]', 'Metal', 'Rap Metal', 'Eb Drop Db', 'Eb Drop Db', 'Eb Drop Db', 'Eb Drop Db', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:33', 213, 2017, '2.2', 77, 55, 0, 0, 75, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.discogs.com/VlNrpfIlrG7dRyEPFfqlJKH_sd0PVEkhDxt_qQfrfTE/rs:fit/g:sm/q:90/h:599/w:599/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEwOTMw/OTEyLTE1MDY3MTgz/MDEtNjMxMS5qcGVn.jpeg', 1695340800000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_20cfe2b18fe34c1b86f7afce2c934b0c', 'library', 22085, 'Jaded', 'Aerosmith', 'Just Push Play', 'Sinomod', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '3:45', 225, 2001, '3', 4402, 357, 5, 0, 1447, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 'https://images.rapgenius.com/b49790bfeebea5616e3b733886cc2be0.934x928x1.jpg', 1461542400000, 1773273600000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_9e752b6ccf6d4556a202f06468af5a1e', 'library', 99069, 'Johnny B.', 'The Hooters', 'One Way Home', 'Manch', '[]', 'Rock', 'Classic Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc"]', '4:09', 249, 1987, '1', 60, 50, 4, 0, 60, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 'https://i.scdn.co/image/ab67616d0000b273be127745dc23ededb57dcd9b', 1773187200000, 1773187200000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_3f34c66c0acc4f94a6ab68b6dee2b508', 'library', 99068, 'Boku Wa Maou', 'Aicle.', 'Dokuiro Marbel', 'JokerTheAnarchist', '[]', 'Rock', NULL, 'E Standard', 'E Standard', NULL, NULL, '["lead"]', '["pc","mac"]', '4:22', 262, 2007, '1', 28, 22, 0, 0, 27, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 'https://www.spirit-of-metal.com/les%20goupes/A/Aicle/Dokuiro%20Marbel/Dokuiro%20Marbel.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_27ecffd7bd8746c28b00da2a7757d04d', 'library', 99067, 'Typical Fashion', 'Love Lost But Not Forgotten', 'Upon the Right, I Saw a New Misery', 'jamesiety', '[]', 'Metal', 'Metalcore', 'D Drop C', 'D Drop C', NULL, 'D Drop C', '["lead","bass","voice"]', '["pc","mac"]', '1:41', 101, 2002, '1', 25, 33, 5, 0, 24, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://f4.bcbits.com/img/a2328027614_16.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_0507104fa1824cb7adb51ec3c9f89f1e', 'library', 99066, 'Mista Bone', 'Great White', 'Twice Shy', 'liteofspace', '[]', 'Rock', 'Hard Rock', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '5:26', 326, 1989, '1', 76, 49, 7, 0, 74, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://m.media-amazon.com/images/I/71YxAt9i1bL._SS500_.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_6a508c2c2c9a48faa4cd3864e1a60615', 'library', 99065, 'Where Dragons Rule', 'DragonForce', 'Valley of the Damned', 'Lord Sai', '[]', 'Metal', 'Power Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '6:01', 361, 2003, '1', 71, 40, 0, 0, 69, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/a/a9/DragonForce-ValleyOfTheDamned-AlbumCover.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_9f64eccba6e74dd7b915beac9194a590', 'library', 99064, 'Evening Star', 'DragonForce', 'Valley of the Damned', 'Lord Sai', '[]', 'Metal', 'Power Metal', 'E Standard', 'E Standard', NULL, NULL, '["lead","voice"]', '["pc","mac"]', '6:49', 409, 2003, '1', 61, 39, 0, 0, 60, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://upload.wikimedia.org/wikipedia/en/a/a9/DragonForce-ValleyOfTheDamned-AlbumCover.jpg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_294f53afe5d845f48ddc1dc5b27d1639', 'library', 99063, 'I''ve Been Waiting', 'Lucille Two', 'Lucille Two', 'Boltimoore', '[]', 'Rock', 'Indie Pop', 'E Standard', NULL, NULL, 'E Standard', '["bass","voice"]', '["pc","mac"]', '4:02', 242, 2024, '1', 32, 17, 1, 0, 32, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273d43d27d52a9df5ff4e5d74d2', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_ad0de0abeef146c8b5a687547cfbf5f0', 'library', 99062, 'Scatman (ski-ba-bop-ba-dop-bop)', 'Scatman John', 'Scatman''s World', 'Emmy-Dell', '[]', 'Pop', NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:43', 223, 1995, '1', 261, 148, 3, 0, 261, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273826ecb816b4126f849732ff0', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_78ce2a004bc541e2a8c7e9f3227ffdb4', 'library', 99061, 'Ravenblack', 'Edguy', 'Monuments', 'MCHBass', '[]', 'Metal', NULL, 'D Standard', NULL, NULL, 'D Standard', '["bass"]', '["pc"]', '5:07', 307, 2017, '1', 25, 22, 1, 0, 25, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 'https://i.discogs.com/OIf0Q29TGZI9QnQdAbCFp1EgRXnjEQ9FQNRB7x_9PFs/rs:fit/g:sm/q:90/h:597/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEwNTYz/MjY5LTE0OTk5NjE2/MjAtODAwNS5qcGVn.jpeg', 1773187200000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fdbf32ab43fc453993cd68f5568902cb', 'library', 99051, 'All In Now', 'Dogstar', 'All In Now', 'shotfirer', '[]', 'Rock', 'Alternative Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc","mac"]', '2:56', 176, 2026, '1.1', 180, 161, 5, 0, 177, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 'https://cdn.shopify.com/s/files/1/0867/1120/6219/files/2000x2000bb_4bfd04b6-0890-458a-8b4f-372ff9460af1.jpg', 1773014400000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_fa0d13c3950e4a048f6099938a42bbab', 'library', 99020, 'The Saw Is The Law', 'Sodom', 'Better Off Dead', 'Sinomod', '[]', 'Metal', 'Heavy Metal', 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass","voice"]', '["pc"]', '4:23', 263, 1990, '1.1', 99, 91, 7, 0, 99, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/b/ba/Sodom-betteroffdead.jpg', 1772841600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_4a479e03e0764e7dae26a2d0ee54833a', 'library', 98954, 'The Prince', 'Metallica', '...And Justice For All', 'RadicalTyrant', '[]', 'Metal', NULL, 'E Standard', 'E Standard', 'E Standard', 'E Standard', '["lead","rhythm","bass"]', '["pc","mac"]', '4:26', 266, 1988, '2', 276, 278, 2, 0, 272, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 'https://i.ibb.co/fYKzJQyL/Metallica-Harvester-of-Sorrow-cover.jpg', 1772409600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_c03ef498ee9b48e7b69e09f767d88c0d', 'library', 95440, 'Another Brick In The Wall Pt. 2', 'Pink Floyd', 'The Wall', 'Emmy-Dell', '[]', 'Rock', 'Classic Rock', 'E Standard | Drop D', 'E Standard', 'E Standard', 'Drop D', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '4:12', 252, 1979, '1.1', 2161, 1198, 5, 0, 2132, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b273cbf8b2cd89b54a021db51e75', 1753833600000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_aef3bbafbb29476d9cf7a318ca71693c', 'library', 94829, 'Piano Lessons', 'Porcupine Tree', 'Stupid Dream', 'shiraishi2012', '[]', 'Rock', 'Progressive Rock', 'E Standard', 'E Standard', NULL, 'E Standard', '["lead","bass","voice"]', '["pc"]', '4:31', 271, 1999, '1.4', 249, 147, 1, 0, 248, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://lastfm.freetls.fastly.net/i/u/500x500/1e48bfd86d204cbcad9b3662debfd9f0.jpg', 1750550400000, 1773187200000, 1773439712094, 1773509466992, 1773439712094, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_e0caf472dd0749499c1345e92b615dab', 'library', 77263, 'Claw The Clouds', 'Satariel', 'Hydra', 'squirrelperson', '[]', 'Metal', 'Melodic Death Metal', 'Custom Tuning', 'Custom Tuning', 'Custom Tuning', NULL, '["lead","rhythm","voice"]', '["pc","mac"]', '3:58', 238, 2005, '2', 63, 21, 2, 0, 59, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 'https://www.metal-archives.com/images/7/7/1/2/77121.jpg', 1683590400000, 1773187200000, 1773439711445, 1773509465896, 1773439711445, 1773509465896); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_2fee2eedcd5f4dc68e4d2a1962d04d0e', 'library', 76612, 'Otsego Amigo', 'Static-X', 'Start a War', 'Mage_Gage', '[]', 'Metal', 'Industrial', 'C Drop Bb', 'C Drop Bb', 'C Drop Bb', 'C Drop Bb', '["lead","rhythm","bass","voice"]', '["pc"]', '2:45', 165, 2005, '1.4', 277, 80, 1, 0, 251, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 'https://upload.wikimedia.org/wikipedia/en/a/a4/Start_a_war.jpg', 1681344000000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); +INSERT INTO catalog_songs (id, source, source_song_id, title, artist_name, album_name, creator_name, artists_ft_json, genre_name, subgenre_name, tuning_summary, lead_tuning_name, rhythm_tuning_name, bass_tuning_name, parts_json, platforms_json, duration_text, duration_seconds, year, version_text, downloads, views, comments_count, reports_count, collected_count, has_lyrics, has_lead, has_rhythm, has_bass, has_vocals, has_bonus_arrangements, has_alternate_arrangements, is_disabled, is_abandoned, is_trending, file_pc_available, file_mac_available, album_art_url, source_created_at, source_updated_at, first_seen_at, last_seen_at, created_at, updated_at) VALUES ('cat_e3c4da0f7af849f190220f3239933717', 'library', 58021, 'Line In The Sand (Evolution Theme)', 'Jim Johnston', 'WWE: The Music, Vol. 6', 'Emmy-Dell', '["Motörhead"]', 'Rock', 'Hard Rock', 'Eb Standard', 'Eb Standard', 'Eb Standard', 'Eb Standard', '["lead","rhythm","bass","voice"]', '["pc","mac"]', '3:44', 224, 2004, '2', 683, 118, 0, 0, 658, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 'https://i.scdn.co/image/ab67616d0000b27325d6ca20037122522b1cb8a4', 1617062400000, 1773187200000, 1773439711445, 1773509466992, 1773439711445, 1773509466992); diff --git a/docs/deployment-workflow.md b/docs/deployment-workflow.md index e3d86c7..f2b97f2 100644 --- a/docs/deployment-workflow.md +++ b/docs/deployment-workflow.md @@ -73,6 +73,8 @@ The Durable Object migration for `ChannelPlaylistDurableObject` is already decla Set these in `.env.deploy` before a real deployment: - `APP_URL` +- `SENTRY_ENVIRONMENT` +- `SENTRY_TRACES_SAMPLE_RATE` (optional) - `CLOUDFLARE_D1_DATABASE_ID` - `CLOUDFLARE_SESSION_KV_ID` - `TWITCH_CLIENT_ID` @@ -111,6 +113,7 @@ echo "" | npx wrangler secret put TWITCH_CLIENT_SECRET --c echo "" | npx wrangler secret put TWITCH_EVENTSUB_SECRET --config wrangler.jsonc echo "" | npx wrangler secret put SESSION_SECRET --config wrangler.jsonc echo "" | npx wrangler secret put ADMIN_TWITCH_USER_IDS --config wrangler.jsonc +echo "" | npx wrangler secret put SENTRY_DSN --config wrangler.jsonc ``` Backend Worker (`request-bot-backend`) required secrets: @@ -119,6 +122,7 @@ Backend Worker (`request-bot-backend`) required secrets: echo "" | npx wrangler secret put TWITCH_CLIENT_ID --config wrangler.aux.jsonc echo "" | npx wrangler secret put TWITCH_CLIENT_SECRET --config wrangler.aux.jsonc echo "" | npx wrangler secret put TWITCH_EVENTSUB_SECRET --config wrangler.aux.jsonc +echo "" | npx wrangler secret put SENTRY_DSN --config wrangler.aux.jsonc ``` Notes: @@ -128,6 +132,11 @@ Notes: - `SESSION_KV` is a KV binding, not a secret - `TWITCH_BOT_USERNAME` and `TWITCH_SCOPES` are not secrets - `ADMIN_TWITCH_USER_IDS` is only needed by the frontend Worker +- Sentry is enabled whenever `SENTRY_DSN` is present +- local development can use a personal test DSN in `.env` +- `SENTRY_DSN` should be set on both Workers so frontend/server routes and backend worker errors report to the same Sentry project +- `SENTRY_TRACES_SAMPLE_RATE` is optional and defaults to `0` +- release tagging uses Cloudflare Worker version metadata automatically, with `SENTRY_RELEASE` available as an override if you want one ### Initialize remote data @@ -148,8 +157,41 @@ If you only need migrations: npm run db:migrate:remote ``` +Use the remote migration commands above for initial environment setup and deliberate operator-driven maintenance. +Once GitHub production deploys are enabled, normal production schema changes should land through pull requests and be migrated by CI after merge to `main`. + +Remote-affecting npm scripts are blocked outside CI by default and print a helper that points contributors back to the local workflow. +If you intentionally need an operator-only override, run: + +```bash +ALLOW_REMOTE_OPERATIONS=1 npm run db:migrate:remote +``` + +The same override applies to: + +- `npm run db:seed:sample:remote` +- `npm run db:bootstrap:remote` +- `npm run deploy` + The sample seed works remotely without an explicit SQL transaction wrapper. Remote D1 rejects uploaded seed files that include `BEGIN TRANSACTION` / `COMMIT`. +### Local migrations for contributors + +Contributors should apply schema changes to local D1 only: + +```bash +npm run db:migrate +``` + +For a clean local rebuild with sample data: + +```bash +npm run db:bootstrap:local +``` + +Use `db:migrate` for normal local schema updates. +Use `db:bootstrap:local` when you want to reset local state completely. + ### Deploy from your machine Preferred: @@ -213,6 +255,7 @@ Add these repository variables: - `TWITCH_BOT_USERNAME` - `TWITCH_SCOPES` +- `SENTRY_ENVIRONMENT` Use the deployed public URL here, not your local tunnel URL. @@ -240,6 +283,7 @@ gh secret set CLOUDFLARE_SESSION_KV_ID gh secret set APP_URL --body "https://your-production-url.example" gh variable set TWITCH_BOT_USERNAME --body "your_bot_username" gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels channel:bot" +gh variable set SENTRY_ENVIRONMENT --body "production" ``` #### Production deploy @@ -247,10 +291,16 @@ gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels channe The production workflow: - runs lint, typecheck, tests, and build +- applies remote D1 migrations - generates built production Wrangler configs - deploys backend first - deploys frontend second +Recommended practice: + +- do not apply production D1 migrations from contributor machines +- let GitHub Actions apply remote migrations during the production deploy workflow after changes land on `main` + #### Preview deploys The preview workflow: @@ -258,12 +308,16 @@ The preview workflow: - creates preview Worker names per PR - rewrites service bindings so preview frontend talks to preview backend - builds the app before generating preview deploy configs +- removes backend cron triggers from preview Workers +- removes backend queue consumers from preview Workers - deploys both preview Workers Important: - preview Worker names are isolated - D1, KV, and Queue resources are not automatically created per preview +- preview deployments do not register scheduled cron triggers, which avoids Cloudflare account cron limits for per-PR Workers +- preview deployments do not register queue consumers, which avoids conflicts with the single consumer attached to the shared queue - use a dedicated staging resource set before relying on preview deploys for contributors ### First deployment checklist @@ -273,7 +327,7 @@ Important: 3. Copy `.env.deploy.example` to `.env.deploy` 4. Set `CLOUDFLARE_D1_DATABASE_ID` and `CLOUDFLARE_SESSION_KV_ID` in `.env.deploy` 5. Set Worker secrets with `wrangler secret put` -6. Set the other required `.env.deploy` values, including `APP_URL` +6. Set the other required `.env.deploy` values, including `APP_URL` and `SENTRY_ENVIRONMENT=production` 7. Run `npm run db:bootstrap:remote` 8. Run `npm run deploy` 9. If you started with a temporary `APP_URL`, update it in `.env.deploy` to the real deployed URL and run `npm run deploy` again diff --git a/drizzle/0001_true_ares.sql b/drizzle/0001_true_ares.sql new file mode 100644 index 0000000..b4a419f --- /dev/null +++ b/drizzle/0001_true_ares.sql @@ -0,0 +1 @@ +ALTER TABLE `catalog_songs` DROP COLUMN `source_url`; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 655be9a..c77396e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1773870000000, "tag": "0000_sturdy_hobgoblin", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1773950400000, + "tag": "0001_true_ares", + "breakpoints": true } ] } diff --git a/package-lock.json b/package-lock.json index 3cf3d48..ca74989 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", + "@sentry/cloudflare": "^10.45.0", "@tanstack/react-form": "latest", "@tanstack/react-query": "latest", "@tanstack/react-query-devtools": "latest", @@ -50,6 +51,9 @@ "typescript": "latest", "vite": "^7.3.1", "vitest": "latest" + }, + "engines": { + "node": ">=22" } }, "node_modules/@alloc/quick-lru": { @@ -2157,6 +2161,15 @@ "node": ">=20.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -5609,6 +5622,36 @@ "win32" ] }, + "node_modules/@sentry/cloudflare": { + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry/cloudflare/-/cloudflare-10.45.0.tgz", + "integrity": "sha512-4rXUCSnBu9MITm7Uj27tYxEhmHczamdMLfeipBjvgeFBJ3LUJBKqrfolCOL5d3q9in/CopgyhMxY7pgUwlCJ8w==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@sentry/core": "10.45.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.x" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@sentry/core": { + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.45.0.tgz", + "integrity": "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", diff --git a/package.json b/package.json index 6227df6..fa7156d 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,18 @@ "format": "node scripts/run-biome.mjs format-write", "deploy:prepare": "node scripts/write-deploy-configs.mjs --mode production --artifact build", "deploy:prepare:source": "node scripts/write-deploy-configs.mjs --mode production --artifact source", - "deploy:frontend": "wrangler deploy --config .generated/wrangler.production.jsonc", - "deploy:backend": "wrangler deploy --config .generated/wrangler.aux.production.jsonc", - "deploy": "npm run build && npm run deploy:prepare && npm run deploy:backend && npm run deploy:frontend", + "deploy:frontend": "node scripts/run-remote-operation.mjs deploy:frontend", + "deploy:backend": "node scripts/run-remote-operation.mjs deploy:backend", + "deploy": "node scripts/run-remote-operation.mjs deploy", "cf-typegen": "wrangler types", "db:generate": "drizzle-kit generate", "db:migrate": "wrangler d1 migrations apply request_bot --local", - "db:migrate:remote": "npm run deploy:prepare:source && wrangler d1 migrations apply request_bot --remote --config .generated/wrangler.production.jsonc", + "db:migrate:remote": "node scripts/run-remote-operation.mjs db:migrate:remote", "db:reset:local": "node scripts/reset-local-d1.mjs", "db:seed:sample:local": "wrangler d1 execute request_bot --local --file data/sample-catalog.sql", - "db:seed:sample:remote": "npm run deploy:prepare:source && wrangler d1 execute request_bot --remote --config .generated/wrangler.production.jsonc --file data/sample-catalog.sql", + "db:seed:sample:remote": "node scripts/run-remote-operation.mjs db:seed:sample:remote", "db:bootstrap:local": "npm run db:reset:local && npm run db:migrate && npm run db:seed:sample:local", - "db:bootstrap:remote": "npm run db:migrate:remote && npm run db:seed:sample:remote", + "db:bootstrap:remote": "node scripts/run-remote-operation.mjs db:bootstrap:remote", "typecheck": "tsc --noEmit", "test": "vitest run --config vitest.config.ts", "test:watch": "vitest --config vitest.config.ts", @@ -46,6 +46,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", + "@sentry/cloudflare": "^10.45.0", "@tanstack/react-form": "latest", "@tanstack/react-query": "latest", "@tanstack/react-query-devtools": "latest", diff --git a/scripts/run-remote-operation.mjs b/scripts/run-remote-operation.mjs new file mode 100644 index 0000000..b3dd6af --- /dev/null +++ b/scripts/run-remote-operation.mjs @@ -0,0 +1,85 @@ +import { execSync } from "node:child_process"; + +const operations = { + "db:migrate:remote": { + localAlternative: "npm run db:migrate", + commands: [ + "npm run deploy:prepare:source", + "wrangler d1 migrations apply request_bot --remote --config .generated/wrangler.production.jsonc", + ], + }, + "db:seed:sample:remote": { + localAlternative: "npm run db:seed:sample:local", + commands: [ + "npm run deploy:prepare:source", + "wrangler d1 execute request_bot --remote --config .generated/wrangler.production.jsonc --file data/sample-catalog.sql", + ], + }, + "db:bootstrap:remote": { + localAlternative: "npm run db:bootstrap:local", + commands: [ + "npm run deploy:prepare:source", + "wrangler d1 migrations apply request_bot --remote --config .generated/wrangler.production.jsonc", + "wrangler d1 execute request_bot --remote --config .generated/wrangler.production.jsonc --file data/sample-catalog.sql", + ], + }, + "deploy:backend": { + localAlternative: null, + commands: ["wrangler deploy --config .generated/wrangler.aux.production.jsonc"], + }, + "deploy:frontend": { + localAlternative: null, + commands: ["wrangler deploy --config .generated/wrangler.production.jsonc"], + }, + deploy: { + localAlternative: null, + commands: [ + "npm run build", + "npm run deploy:prepare", + "wrangler deploy --config .generated/wrangler.aux.production.jsonc", + "wrangler deploy --config .generated/wrangler.production.jsonc", + ], + }, +}; + +const operationName = process.argv[2]; + +if (!operationName || !(operationName in operations)) { + console.error("Unknown remote operation."); + process.exit(1); +} + +const operation = operations[operationName]; +const inCi = + process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; +const allowOverride = process.env.ALLOW_REMOTE_OPERATIONS === "1"; + +if (!inCi && !allowOverride) { + console.error(""); + console.error(`Remote operation "${operationName}" is blocked outside CI.`); + console.error(""); + + if (operation.localAlternative) { + console.error("Use this instead for local development:"); + console.error(` ${operation.localAlternative}`); + console.error(""); + } + + console.error( + "Production migrations and deploys are handled by GitHub Actions" + ); + console.error("when changes are merged to main."); + console.error(""); + console.error( + "If you intentionally need a maintainer override, rerun with:" + ); + console.error(` ALLOW_REMOTE_OPERATIONS=1 npm run ${operationName}`); + console.error(""); + process.exit(1); +} + +for (const command of operation.commands) { + execSync(command, { + stdio: "inherit", + }); +} diff --git a/scripts/write-deploy-configs.mjs b/scripts/write-deploy-configs.mjs index a3bc2a2..24d3d80 100644 --- a/scripts/write-deploy-configs.mjs +++ b/scripts/write-deploy-configs.mjs @@ -108,6 +108,21 @@ const backendName = frontendConfig.name = frontendName; backendConfig.name = backendName; + +if (mode === "preview" && backendConfig.triggers?.crons) { + backendConfig.triggers = { + ...backendConfig.triggers, + crons: [], + }; +} + +if (mode === "preview" && backendConfig.queues?.consumers) { + backendConfig.queues = { + ...backendConfig.queues, + consumers: [], + }; +} + frontendConfig.assets = frontendConfig.assets ? { ...frontendConfig.assets, diff --git a/src/lib/db/client.ts b/src/lib/db/client.ts index c85047a..0163a1e 100644 --- a/src/lib/db/client.ts +++ b/src/lib/db/client.ts @@ -1,7 +1,8 @@ import { drizzle } from "drizzle-orm/d1"; import type { AppEnv } from "~/lib/env"; +import { getSentryD1Database } from "~/lib/sentry"; import * as schema from "./schema"; export function getDb(env: AppEnv) { - return drizzle(env.DB, { schema }); + return drizzle(getSentryD1Database(env), { schema }); } diff --git a/src/lib/db/latest-migration.generated.ts b/src/lib/db/latest-migration.generated.ts index 5180d4d..d873021 100644 --- a/src/lib/db/latest-migration.generated.ts +++ b/src/lib/db/latest-migration.generated.ts @@ -1 +1 @@ -export const LATEST_MIGRATION_NAME = "0000_sturdy_hobgoblin.sql"; +export const LATEST_MIGRATION_NAME = "0001_true_ares.sql"; diff --git a/src/lib/db/repositories.ts b/src/lib/db/repositories.ts index f13d5c0..1888434 100644 --- a/src/lib/db/repositories.ts +++ b/src/lib/db/repositories.ts @@ -11,6 +11,7 @@ import { createId, decodeHtmlEntities, encodeHtmlEntities, + normalizeSongSourceUrl, parseJsonStringArray, slugify, } from "~/lib/utils"; @@ -46,7 +47,6 @@ import { } from "./schema"; const CATALOG_SONG_MUTABLE_FIELDS: Array = [ - "sourceUrl", "title", "artistName", "albumName", @@ -1024,7 +1024,6 @@ export async function searchCatalogSongs( sourceUpdatedAt: number | null; downloads: number; hasLyrics: number; - sourceUrl: string; source: string; relevance: number; }>(sql` @@ -1050,7 +1049,6 @@ export async function searchCatalogSongs( catalog_songs.source_updated_at AS sourceUpdatedAt, catalog_songs.downloads, catalog_songs.has_lyrics AS hasLyrics, - catalog_songs.source_url AS sourceUrl, catalog_songs.source, (${relevanceSql}) AS relevance FROM catalog_songs @@ -1083,7 +1081,10 @@ export async function searchCatalogSongs( hasLyrics: !!row.hasLyrics, downloads: row.downloads, source: row.source, - sourceUrl: row.sourceUrl, + sourceUrl: normalizeSongSourceUrl({ + source: row.source, + sourceId: row.sourceSongId, + }), score: row.relevance, })), total: totalRows[0]?.count ?? 0, @@ -1120,10 +1121,36 @@ export async function getCatalogSongBySourceId( hasLyrics: !!row.hasLyrics, downloads: row.downloads, source: row.source, - sourceUrl: row.sourceUrl, + sourceUrl: normalizeSongSourceUrl({ + source: row.source, + sourceId: row.sourceSongId, + }), }; } +export async function getCatalogSongsByIds(env: AppEnv, songIds: string[]) { + const uniqueIds = [...new Set(songIds.filter(Boolean))]; + if (uniqueIds.length === 0) { + return []; + } + + const rows = await getDb(env).query.catalogSongs.findMany({ + where: inArray(catalogSongs.id, uniqueIds), + }); + + return rows.map((row) => ({ + id: row.id, + sourceId: row.sourceSongId, + source: row.source, + sourceUrl: normalizeSongSourceUrl({ + source: row.source, + sourceId: row.sourceSongId, + }), + sourceUpdatedAt: row.sourceUpdatedAt ?? undefined, + downloads: row.downloads, + })); +} + export async function getCatalogSearchFilterOptions(env: AppEnv) { const db = getDb(env); @@ -1180,7 +1207,6 @@ export async function upsertCatalogSongs( id: string; source: string; sourceSongId: number; - sourceUrl: string; title: string; artistName: string; albumName: string | null; @@ -1228,7 +1254,6 @@ export async function upsertCatalogSongs( id, source, source_song_id AS sourceSongId, - source_url AS sourceUrl, title, artist_name AS artistName, album_name AS albumName, @@ -1305,7 +1330,6 @@ export async function upsertCatalogSongs( await db .update(catalogSongs) .set({ - sourceUrl: song.sourceUrl, title: song.title, artistName: song.artistName, albumName: song.albumName ?? null, diff --git a/src/lib/db/schema-version.ts b/src/lib/db/schema-version.ts index df45590..cf169f3 100644 --- a/src/lib/db/schema-version.ts +++ b/src/lib/db/schema-version.ts @@ -1,9 +1,29 @@ import type { AppEnv, BackendEnv } from "~/lib/env"; +import { getSentryD1Database } from "~/lib/sentry"; import { LATEST_MIGRATION_NAME } from "./latest-migration.generated"; const SCHEMA_CHECK_TTL_MS = 30_000; +const COMPATIBLE_MIGRATION_NAMES = new Set([LATEST_MIGRATION_NAME]); -type DatabaseEnv = Pick | Pick; +type DatabaseEnv = + | Pick< + AppEnv, + | "DB" + | "CF_VERSION_METADATA" + | "SENTRY_DSN" + | "SENTRY_ENVIRONMENT" + | "SENTRY_RELEASE" + | "SENTRY_TRACES_SAMPLE_RATE" + > + | Pick< + BackendEnv, + | "DB" + | "CF_VERSION_METADATA" + | "SENTRY_DSN" + | "SENTRY_ENVIRONMENT" + | "SENTRY_RELEASE" + | "SENTRY_TRACES_SAMPLE_RATE" + >; let cachedSchemaCheck: | { @@ -30,11 +50,12 @@ export async function assertDatabaseSchemaCurrent(env: DatabaseEnv) { } let latestMigrationName: string | null = null; + const db = getSentryD1Database(env); try { - const result = await env.DB.prepare( - "select name from d1_migrations order by id desc limit 1" - ).first<{ name: string }>(); + const result = (await db + .prepare("select name from d1_migrations order by id desc limit 1") + .first()) as { name?: string } | null; latestMigrationName = result?.name ?? null; } catch (error) { throw new DatabaseSchemaOutOfDateError( @@ -45,7 +66,10 @@ export async function assertDatabaseSchemaCurrent(env: DatabaseEnv) { ); } - if (latestMigrationName !== LATEST_MIGRATION_NAME) { + if ( + !latestMigrationName || + !COMPATIBLE_MIGRATION_NAMES.has(latestMigrationName) + ) { throw new DatabaseSchemaOutOfDateError( getSchemaMismatchMessage( `Expected ${LATEST_MIGRATION_NAME} but found ${latestMigrationName ?? "no applied migrations"}.` diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index a2c3328..41ff31d 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -535,7 +535,6 @@ export const catalogSongs = sqliteTable( id: text("id").primaryKey(), source: text("source").notNull().default("library"), sourceSongId: integer("source_song_id").notNull(), - sourceUrl: text("source_url").notNull(), title: text("title").notNull(), artistName: text("artist_name").notNull(), albumName: text("album_name"), diff --git a/src/lib/env.ts b/src/lib/env.ts index 4aed211..f9a621e 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -4,6 +4,11 @@ export interface AppEnv { SESSION_KV: KVNamespace; BACKEND_SERVICE: Fetcher; TWITCH_REPLY_QUEUE: Queue; + CF_VERSION_METADATA?: WorkerVersionMetadata; + SENTRY_DSN?: string; + SENTRY_ENVIRONMENT?: string; + SENTRY_RELEASE?: string; + SENTRY_TRACES_SAMPLE_RATE?: string; TWITCH_CLIENT_ID: string; TWITCH_CLIENT_SECRET: string; TWITCH_EVENTSUB_SECRET: string; @@ -17,6 +22,11 @@ export interface BackendEnv { APP_URL: string; DB: D1Database; CHANNEL_PLAYLIST_DO: DurableObjectNamespace; + CF_VERSION_METADATA?: WorkerVersionMetadata; + SENTRY_DSN?: string; + SENTRY_ENVIRONMENT?: string; + SENTRY_RELEASE?: string; + SENTRY_TRACES_SAMPLE_RATE?: string; TWITCH_CLIENT_ID: string; TWITCH_CLIENT_SECRET: string; TWITCH_EVENTSUB_SECRET: string; diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 0000000..18b8e8c --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,83 @@ +import type { CloudflareOptions } from "@sentry/cloudflare"; +import { instrumentD1WithSentry } from "@sentry/cloudflare"; + +type SentryRuntimeEnv = { + DB: D1Database; + SENTRY_DSN?: string; + SENTRY_ENVIRONMENT?: string; + SENTRY_TRACES_SAMPLE_RATE?: string; + SENTRY_RELEASE?: string; + CF_VERSION_METADATA?: WorkerVersionMetadata; +}; + +const instrumentedDatabases = new WeakMap(); + +export function getSentryOptions( + env: Pick< + SentryRuntimeEnv, + | "SENTRY_DSN" + | "SENTRY_ENVIRONMENT" + | "SENTRY_TRACES_SAMPLE_RATE" + | "SENTRY_RELEASE" + | "CF_VERSION_METADATA" + > +): CloudflareOptions | undefined { + if (!shouldEnableSentry(env)) { + return undefined; + } + + return { + dsn: env.SENTRY_DSN, + environment: getSentryEnvironment(env.SENTRY_ENVIRONMENT), + release: getSentryRelease(env), + tracesSampleRate: parseSampleRate(env.SENTRY_TRACES_SAMPLE_RATE), + sendDefaultPii: false, + }; +} + +export function getSentryD1Database(env: SentryRuntimeEnv) { + if (!shouldEnableSentry(env)) { + return env.DB; + } + + const cached = instrumentedDatabases.get(env.DB); + if (cached) { + return cached; + } + + const instrumented = instrumentD1WithSentry(env.DB); + instrumentedDatabases.set(env.DB, instrumented); + return instrumented; +} + +function shouldEnableSentry(env: Pick) { + return typeof env.SENTRY_DSN === "string" && env.SENTRY_DSN.length > 0; +} + +function parseSampleRate(value?: string) { + if (!value) { + return 0; + } + + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) { + return 0; + } + + return parsed; +} + +function getSentryRelease( + env: Pick +) { + return ( + env.SENTRY_RELEASE || + env.CF_VERSION_METADATA?.id || + env.CF_VERSION_METADATA?.tag + ); +} + +function getSentryEnvironment(value?: string) { + const normalized = value?.trim().toLowerCase(); + return normalized || "development"; +} diff --git a/src/lib/twitch/bot.ts b/src/lib/twitch/bot.ts index 21650f8..9ac7a49 100644 --- a/src/lib/twitch/bot.ts +++ b/src/lib/twitch/bot.ts @@ -13,6 +13,7 @@ import { } from "~/lib/db/repositories"; import * as schema from "~/lib/db/schema"; import type { AppEnv, BackendEnv } from "~/lib/env"; +import { getSentryD1Database } from "~/lib/sentry"; import { createEventSubSubscription, deleteEventSubSubscription, @@ -40,7 +41,7 @@ function asAppEnv(env: RuntimeEnv) { } function getDb(env: RuntimeEnv) { - return drizzle(env.DB, { schema }); + return drizzle(getSentryD1Database(env), { schema }); } async function ensureSubscription(input: { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 40522f6..21679d8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -124,3 +124,19 @@ export function parseJsonStringArray(input: string | null | undefined) { return []; } } + +const CUSTOMSFORGE_IGNITION_BASE_URL = "https://ignition4.customsforge.com"; + +export function normalizeSongSourceUrl(input: { + source?: string | null; + sourceUrl?: string | null; + sourceId?: number | null; +}) { + const sourceId = input.sourceId ?? null; + + if (sourceId != null) { + return `${CUSTOMSFORGE_IGNITION_BASE_URL}/cdlc/${sourceId}`; + } + + return input.sourceUrl?.trim() || undefined; +} diff --git a/src/routes/api/dashboard/playlist/route.ts b/src/routes/api/dashboard/playlist/route.ts index 053063e..a0a7446 100644 --- a/src/routes/api/dashboard/playlist/route.ts +++ b/src/routes/api/dashboard/playlist/route.ts @@ -4,6 +4,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { getSessionUserId } from "~/lib/auth/session.server"; import { callBackend } from "~/lib/backend"; import { + getCatalogSongsByIds, getChannelSettingsByChannelId, getDashboardChannelAccess, getDashboardState, @@ -74,6 +75,32 @@ async function requireDashboardState( }; } +async function enrichPlaylistItems( + runtimeEnv: AppEnv, + items: Array> +) { + const songIds = items + .map((item) => (typeof item.songId === "string" ? item.songId : null)) + .filter((songId): songId is string => Boolean(songId)); + + const catalogSongs = await getCatalogSongsByIds(runtimeEnv, songIds); + const catalogById = new Map(catalogSongs.map((song) => [song.id, song])); + + return items.map((item) => { + const songId = typeof item.songId === "string" ? item.songId : null; + const catalogSong = songId ? catalogById.get(songId) : null; + + return { + ...item, + songCatalogSourceId: + item.songCatalogSourceId ?? catalogSong?.sourceId ?? null, + songUrl: item.songUrl ?? catalogSong?.sourceUrl ?? null, + songSourceUpdatedAt: catalogSong?.sourceUpdatedAt ?? null, + songDownloads: catalogSong?.downloads ?? null, + }; + }); +} + export const Route = createFileRoute("/api/dashboard/playlist")({ server: { handlers: { @@ -94,7 +121,7 @@ export const Route = createFileRoute("/api/dashboard/playlist")({ return json({ channel: state.channel, playlist: state.playlist, - items: state.items, + items: await enrichPlaylistItems(runtimeEnv, state.items), playedSongs: state.playedSongs, accessRole: state.accessRole, requiredPaths: state.settings diff --git a/src/routes/dashboard/playlist.tsx b/src/routes/dashboard/playlist.tsx index 3d58ccd..33216a4 100644 --- a/src/routes/dashboard/playlist.tsx +++ b/src/routes/dashboard/playlist.tsx @@ -16,11 +16,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { pageTitle } from "~/lib/page-title"; import { formatPathLabel } from "~/lib/request-policy"; -import { getErrorMessage } from "~/lib/utils"; +import { getErrorMessage, normalizeSongSourceUrl } from "~/lib/utils"; type PlaylistItem = { id: string; songId?: string; + songCatalogSourceId?: number | null; songTitle: string; songArtist?: string; songAlbum?: string; @@ -29,6 +30,8 @@ type PlaylistItem = { songPartsJson?: string; songDurationText?: string; songUrl?: string; + songSourceUpdatedAt?: number | null; + songDownloads?: number | null; requestedByTwitchUserId?: string; requestedByLogin?: string; requestedByDisplayName?: string; @@ -360,12 +363,6 @@ function DashboardPlaylistPage() { mutation.isPending && pendingRowAction?.action === "manualAdd" && pendingRowAction.songId === songId; - const isChoosingVersion = (itemId: string, candidateId: string) => - mutation.isPending && - pendingRowAction?.action === "chooseVersion" && - pendingRowAction.itemId === itemId && - pendingRowAction.songId === candidateId; - return (
@@ -663,112 +660,109 @@ function DashboardPlaylistPage() { View {getResolvedCandidates(item).length} version {getResolvedCandidates(item).length === 1 ? "" : "s"} -
-
-
Song
-
Tuning
-
Paths
-
DLC Creator
-
Duration
-
Updated
-
DL Count
-
Actions
-
- {getResolvedCandidates(item).map( - (candidate, candidateIndex) => ( -
-
-

- {candidate.artist - ? `${candidate.artist} - ${candidate.title}` - : candidate.title} -

-
-
- {candidate.tuning ?? "Unknown"} -
-
- {candidate.parts?.length - ? candidate.parts - .map((part) => formatPathLabel(part)) - .join(", ") - : "Unknown"} -
-
- {candidate.creator ?? "Unknown"} -
-
- {candidate.durationText ?? "??:??"} -
-
- {candidate.sourceUpdatedAt - ? formatDate(candidate.sourceUpdatedAt) - : "Unknown"} -
-
- {candidate.downloads != null - ? candidate.downloads.toLocaleString() - : "Unknown"} -
-
- {candidate.sourceUrl ? ( - - ) : null} -
- {item.songUrl ? ( - - ) : null}