- PHP 76.5%
- Twig 11.1%
- JavaScript 7.9%
- CSS 3.5%
- Python 0.7%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| assets | ||
| bin | ||
| config | ||
| docker | ||
| docs | ||
| migrations | ||
| public | ||
| resources | ||
| src | ||
| templates | ||
| tests | ||
| tools | ||
| translations | ||
| .codex | ||
| .dockerignore | ||
| .env.dev | ||
| .env.dist | ||
| .env.docker.example | ||
| .env.test | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| arch_audit.md | ||
| bitbucket-pipelines.yml | ||
| changelog.md | ||
| compose.override.yaml | ||
| compose.yaml | ||
| composer.json | ||
| composer.lock | ||
| context.md | ||
| DOCKER.md | ||
| importmap.php | ||
| package-lock.json | ||
| package.json | ||
| phpunit.dist.xml | ||
| postcss.config.js | ||
| README.md | ||
| symfony.lock | ||
Generation Y Community
AI agents must follow AGENTS.md. Writing, adding, modifying or running any tests, including PHPUnit and JavaScript tests, is prohibited until a separate explicit user command authorizes it. This includes test-running scripts, CI and hooks. Historical test results are records only and do not authorize future test work.
For the migrated Windows Docker environment, see DOCKER.md.
The approved modernization roadmap takes precedence over legacy radio operations below. Execute P01-P24 in order; do not enable AzuraCast jobs during baseline work. See runtime and validation baseline and data ownership and migration safeguards.
Symfony 6.4 application for the Generation Y music discovery community. It provides the official YouTube playlist, track landing pages, ratings, moderated comments, track suggestions, newsletter signup, Android app distribution, analytics, and an Tabler back office with EasyAdmin CRUD behavior.
Requirements
- PHP 8.3+ with the extensions required by Symfony and Doctrine
- Composer 2
- Node.js and npm
- MySQL 8.4
P09 local radio-history cleanup is separately approved and applied in the Windows database. See P09 status and migration gates and recovery. Its migration refuses production and unreviewed targets; a generic migration command is not approval to remove historical data elsewhere.
Editorial changes use explicit transactions and Artist identity links; unpublish preserves history and media. See editorial policy and P10 verification before purging or reviewing historical relations.
Artist directory/profile pages now use SQL totals and pagination. The homepage prepares six initial playlist items and immediately loads the complete queue for its scrollable sidebar, without starting YouTube. Selection and playback update the highlighted row without reordering the list; the list scrolls to follow it once it passes the visible midpoint or leaves view; /listen-playlist retains the complete player payload. See P11 measurements and schema status.
Local setup
cp .env.dist .env.local
docker compose up -d database mercure mailer
composer install
npm ci
npm run tailwind:build
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console asset-map:compile
symfony server:start
Set real local values in .env.local; never commit credentials. At minimum,
review APP_SECRET, DATABASE_URL, and the Mercure
settings. Set APP_CANONICAL_URL to the single public HTTPS origin used by
search engines (production defaults to https://ygen.top). If PHP runs behind
a proxy, set TRUSTED_PROXIES to only the proxy IPs or CIDRs.
Mailpit is available at http://127.0.0.1:8025. Mercure listens on port 3000.
Editorial media now uses read-only rendering and write-time preparation; old interactive blocks require explicit review before scripts run in an isolated frame. Files survive failed saves and form removal. See media lifecycle, trust and recovery for private inventory, bounded backfill and worker isolation safeguards.
Public pages use a separate document/public Twig layout and an explicit read-only page metadata contract. The frontend build produces both public and admin CSS. Administration uses locally pinned Tabler components throughout, with EasyAdmin retained for protected CRUD behavior and its single Bootstrap runtime. See Tabler administration. See P14 architecture and verification.
Public/admin stylesheet sources are independent (assets/styles/app.css and admin.css); public semantic tokens are separate from the Tabler administration theme. npm run tailwind:build builds both entries before replacing outputs. AssetMapper publishes both compiled bundles with content-hashed URLs, and the admin entry uses its logical asset name. Public pages use one PageContainer width/gutter contract; reading text has its own measure. Build both stylesheets and the asset map before activation, retain old hashed assets, and clear the production Twig cache. See P17 validation and deployment status.
Public comment/reply and suggestion forms keep entered text on recoverable failures. Non-JavaScript comment failures return a private retry form without placing body/email in URLs or sessions. Repeated identical comments are deduplicated under a track-row transaction for ten minutes. The playlist API/iframe/readiness lifecycle has bounded waits and manual retry; its page includes server-rendered track links. Deleted update subjects remain descriptive non-link history. See P18 implementation and verification.
Background processes
Durable YouTube metadata operations use Messenger. Run the outbox dispatcher every minute alongside the consumer; see background jobs and recovery. Production consumers remain required:
php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M
Artist biographies are authored through the administrator's structured editorial editor. Existing texts and downloaded photos remain independent catalog content. Wikipedia URLs are optional external references only: the application does not import Wikipedia/Wikidata biographies, match artist identities through them or synchronize track metadata from them. Old queued enrichment messages are acknowledged without external requests or content changes.
Reviewed original biography manuscripts can be published in explicit batches with php bin/console app:editorial:publish-biographies <filename.json>. Files live in resources/editorial; exact artist ID/name/slug checks and version-zero writes preserve existing drafts, published editorials and legacy content.
Analytics retention requires owner-approved policy, a stage restore/reconcile and explicit --execute before scheduling. See metrics, privacy and bounded retention. Preview separately:
php bin/console app:analytics:prune --dry-run --days=400 --batch-size=500 --max-batches=10
Local analytics uses a first-party pseudonymous visitor cookie and 30-minute
sessions. Sources, channels, and devices are attributed from the first
pageview in each session. A lightweight /analytics/heartbeat request keeps
visible pages represented in the five-minute web-user metric without increasing
pageview totals. The player additionally records allow-listed first-party
events for playlist playback and track votes. Generic listening_seconds retains
its historical mixed meaning; video_playlist_listening_seconds measures video.
Retirement never subtracts or resets stored listening totals. The admin dashboard
supports rolling 24-hour and 1-, 3-, 5-, 7-, 30-, 90-, 183-, and 365-day
presets plus custom date ranges of up to 366 days.
The visitor map uses approximate city-level coordinates and never stores
visitor IP addresses. Trusted Cloudflare/X-Geo-* headers take priority; when
they are absent, the application performs an in-memory lookup against a local
GeoLite2 City .mmdb file using the effective Symfony client IP. Existing
sessions without coordinates remain counted but cannot be plotted
retroactively.
GeoLite2 City on production
Create a free MaxMind account and license key, then install the official update client on the FASTPANEL server:
sudo apt update
sudo apt install geoipupdate
sudoedit /etc/GeoIP.conf
Configure /etc/GeoIP.conf without committing its credentials:
AccountID YOUR_MAXMIND_ACCOUNT_ID
LicenseKey YOUR_MAXMIND_LICENSE_KEY
EditionIDs GeoLite2-City
Download the current database into the project so it remains accessible even
when FASTPANEL applies a PHP open_basedir restriction. Replace the example
path with the real project root:
sudo install -d -m 0755 /var/www/USER/data/www/ygen.top/var/geoip
sudo geoipupdate -d /var/www/USER/data/www/ygen.top/var/geoip
sudo chmod 0644 /var/www/USER/data/www/ygen.top/var/geoip/GeoLite2-City.mmdb
ls -lh /var/www/USER/data/www/ygen.top/var/geoip/GeoLite2-City.mmdb
Set this absolute path in the production .env.local:
GEOLITE2_CITY_DB_PATH=/var/www/USER/data/www/ygen.top/var/geoip/GeoLite2-City.mmdb
After clearing the production cache, verify a lookup without persisting the IP:
APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear
APP_ENV=prod APP_DEBUG=0 php bin/console app:analytics:geoip:check 8.8.8.8
Run geoipupdate -d /var/www/USER/data/www/ygen.top/var/geoip weekly from
root's cron or an equivalent system timer so the database remains current. If
FASTPANEL proxies Nginx to Apache/PHP over loopback, retain only that local
proxy in TRUSTED_PROXIES; Symfony will then read the real visitor address from
its trusted forwarded header.
Votes update scores immediately. Top 100 chart history is reconciled in a coalesced one-minute scheduler, including publication changes. The chart displays its last successful history refresh; a vote never waits for the full reconciliation:
* * * * * cd /var/www/ygen && php bin/console app:top-tracks:refresh --env=prod --quiet
The optional realtime analytics publisher is:
php bin/console app:analytics:realtime:worker --interval=5
Its Mercure topic is private; configure subscriber authorization if push updates are required. The admin dashboard retains an authenticated polling fallback.
Official YouTube video discovery
Enable YouTube Data API v3 in a Google Cloud project and keep its API key only
in the ignored .env.local file:
YOUTUBE_API_KEY=
YOUTUBE_TRUSTED_CHANNEL_IDS=
Discover official primary and alternate versions for one Track Page:
php bin/console app:youtube:discover artist-track-slug
php bin/console app:youtube:discover artist-track-slug --apply
The default interactive run shows ranked candidates and asks before writing.
--apply stores only high-confidence results. Existing values require
--force. Use repeatable --trusted-channel=UC... arguments for a label or
distributor channel that an administrator has independently verified. A matching title, a high view count, a Wikipedia reference or YouTube's licensed-content flag alone never makes a user channel trusted.
The ranked-candidate table prints both the channel name and immutable Channel
ID. For every needs_review channel it also prints a ready-to-copy rerun
command containing --trusted-channel=UC.... Use that override only after
checking the channel's YouTube page and an independent official source such as
the artist website, label website, rights-holder catalog, or an established VEVO/Topic
relationship.
YouTube search results are cached in Symfony's cache.app pool for 24 hours;
individual video and channel resources are cached for 7 days; missing resources
are retried after 1 hour. Trusted channel IDs come only from explicit configuration or administrator input. Repeating the same command during those windows therefore
does not consume additional YouTube quota. To deliberately discard this data,
clear the application pool before running discovery again:
php bin/console cache:pool:clear cache.app
Public community activity snapshots
Track and artist changes, Top 100 movement, and approved discussions are written to the immutable public activity log automatically. Run the daily aggregation after midnight to store the previous day's most-viewed track page and community favourite. The command is idempotent for each date:
php bin/console app:community-activity:snapshot
Pass Y-m-d to capture or retry a specific day. The log contains public music
metadata only; it never copies visitor identifiers, IP hashes, email addresses,
or comment author details.
Static analysis and dependency audits
The commands below do not authorize running tests. Follow AGENTS.md.
php bin/console lint:container
php bin/console lint:yaml config
php bin/console lint:twig templates
php bin/console doctrine:schema:validate
composer validate --strict
composer audit --locked
npm audit
Deployment notes
- Use
public/as the only web document root. - Run Doctrine migrations before switching application traffic.
- Keep
public/uploads/on persistent storage and back it up. - Run Messenger consumers under a process supervisor.
- Build CSS and the asset map during deployment.
- Use Redis-backed locks and rate-limit storage before scaling to multiple app nodes; the default file lock is for a single-node deployment.
Search visibility
Public discovery pages are server-rendered and linked through the main navigation:
/listen-playlistfor official YouTube videos/tracksfor the paginated track archive/artistsand/artist/{slug}for artist discovery/top-tracksfor listener rankings/latest-newsfor the paginated track and artist update log/latest-activityfor the paginated chart and community activity log/aboutfor community information
/sitemap.xml is the stable sitemap index submitted through robots.txt and
Search Console. It references /sitemap-pages.xml, /sitemap-tracks.xml, and
/sitemap-artists.xml, and /sitemap-collections.xml; the child documents are cached, support conditional
requests and gzip, and contain only canonical public URLs. Track entries include
eligible local cover images. Automatically created track pages remain noindex
until they have complete artist/title metadata plus a local cover, artist
context, or editorial content. APP_CANONICAL_URL also drives production
redirects from alternate hosts.
AI-readable discovery is available at /llms.txt for the concise curated index
and /llms-full.txt for expanded, self-contained music community context. Both files
use APP_CANONICAL_URL, are served as cacheable UTF-8 plain text, and are
explicitly allowed by public/robots.txt.
For Search Console or Bing Webmaster Tools, set the verification token values
without the surrounding <meta> tag:
GOOGLE_SITE_VERIFICATION=
BING_SITE_VERIFICATION=
Frontend build versions, Windows watcher and browser support: frontend toolchain.
First-party heartbeat, event provenance and playback deduplication: ingest contract.
Stable track identity and old URLs
Persisted technical track IDs are immutable. Edit the track or artist slug to change its current public address; proved old paths redirect directly to the current published entity. Run php bin/console app:identity:audit for a read-only conflict and consumer-column report. Apply the reviewed P06 additive history migration only after backup/restore and ownership preflight. See P06 implementation and verification.
P07 shared music contract
VideoPlaylistController / VideoPlaylistViewData and the video-playlist Stimulus controller own the useful YouTube playlist. Home/feed/chart templates live in home/, feed/, and chart/. TrackRatingController serves canonical /api/track-rating endpoints with direct legacy radio API aliases; the legacy vote POST never redirects. Shared JS transport/artwork and PHP vote/artwork/chart services survive audio retirement. The proved unused duplicate builder was removed after its tests moved to the current catalog builder. No database migration or audio/data retirement is part of P07. See P07 implementation and verification.
Live audio retirement
Live-radio runtime is retired. Existing track/artist pages, local covers, chart and vote history, comments, video playlist and Android downloads remain. /player and the old airplay /playlist return 410, together with the retired now-playing, webhook and now-playing comment API. /video-radio still redirects to /listen-playlist. Stop obsolete scheduler/webhook jobs before deploying this code; do not restore them from historical notes. See the deployment jobs and retirement manifest. Legacy tables remain intact until the separate P09 data gate. Private local env files may retain obsolete keys; the application does not consume them.