MIL-007: Wiki and release #49
@@ -0,0 +1,41 @@
|
||||
# Publish the wiki pages of docs/wiki/ to the wiki of this repository.
|
||||
#
|
||||
# Purpose: keep the wiki (English, Danish, Bahasa Melayu) equal to the folder.
|
||||
# Tool: Gitea Actions; calls the reusable workflow
|
||||
# TirSystem/git-workflows publish-wiki-pages (same release policy as
|
||||
# the framework: the major tag v1 only receives compatible changes).
|
||||
# Triggers: a push to main that changes docs/wiki/ (the protected branch);
|
||||
# there is no manual trigger, so no run can change the wiki by accident.
|
||||
# Input: wiki_dir docs/wiki; prune "true" deletes wiki pages that are not in
|
||||
# the folder.
|
||||
# Secret: TOKEN_FOR_GITEA_WIKI, an organization secret with the scope
|
||||
# write:repository (framework/templates/ci/publish-wiki.yml, step 1).
|
||||
# Changes outside the repository: the wiki of this repository.
|
||||
# Decision: MIL-007 (docs/milestones/mil-007-wiki-and-release.md), task 5.
|
||||
|
||||
---
|
||||
name: Publish wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/wiki/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-wiki
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
wiki:
|
||||
name: Publish wiki pages
|
||||
uses: TirSystem/git-workflows/.gitea/workflows/publish-wiki-pages.yml@v1
|
||||
with:
|
||||
wiki_dir: docs/wiki
|
||||
prune: "true"
|
||||
secrets:
|
||||
TOKEN_FOR_GITEA_WIKI: ${{ secrets.TOKEN_FOR_GITEA_WIKI }}
|
||||
@@ -0,0 +1,8 @@
|
||||
# yamllint configuration of this project (QC-YML-001, criterion 3).
|
||||
---
|
||||
extends: default
|
||||
rules:
|
||||
line-length:
|
||||
max: 100
|
||||
truthy:
|
||||
check-keys: false
|
||||
@@ -30,7 +30,7 @@ Layout of the repository:
|
||||
| `tests/` | The automated tests (pytest); they use a stored chart page and a fake music service, never the network |
|
||||
| `docs/` | Business case, plan, milestones, reviews and the wiki sources |
|
||||
|
||||
The wiki is written in English, Danish and Bahasa Malaysia and explains what each lecture step teaches.
|
||||
The wiki (`docs/wiki/`, published to the repository wiki) is written in English, Danish and Bahasa Malaysia and explains what each lecture step teaches: [English](docs/wiki/Home.md), [Dansk](docs/wiki/Home-da.md), [Bahasa Melayu](docs/wiki/Home-ms.md).
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
|
||||
+61
-61
@@ -1,61 +1,61 @@
|
||||
# Artifact Registry
|
||||
|
||||
This project's artifact state. Types, short names and `CrossReference
|
||||
Candidates` come from the framework catalog
|
||||
(`framework/registry/artifact-catalog.md`); this file only records where
|
||||
each document lives in *this* project and the next version to use.
|
||||
|
||||
Delete rows for types you don't use. Add a row the first time you create a
|
||||
document of a type. `Primary File` may contain a glob (e.g.
|
||||
`docs/use-cases/uc-*/uc.md`); `framework/scripts/find-crossreferences.sh` reads it.
|
||||
|
||||
| Short Name | Artifact Type | Primary File | Next Available Version |
|
||||
| --- | --- | --- | --- |
|
||||
| BC | Business Case | docs/business-case.md | 002 |
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/mil-*.md | 008 |
|
||||
| GOV | Governance / ARB Workflow | docs/sqa/governance.md | 002 |
|
||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 017 |
|
||||
|
||||
## Languages
|
||||
|
||||
Set the PO language when the project starts; `project-planning` asks for it
|
||||
if it is missing. An artifact of a type marked "Written in the PO language"
|
||||
exists once, in that language, under its normal name (`business-case.md`); the
|
||||
`artifact` skill ("One file per artifact") has the rule.
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| PO language | en |
|
||||
| PO domain | it |
|
||||
| High-level register | IT Executive English |
|
||||
| Technical register | IT Professional English |
|
||||
| Wiki languages | en, da, ms (`framework/process/wiki.md`) |
|
||||
|
||||
Every artifact of a type marked "Yes" below states its language and domain in
|
||||
its `Language` and `Domain` Metadata rows; `new-artifact.sh` fills them from
|
||||
`PO language` and `PO domain`. `Language` is a BCP 47 code (`da`, `en`).
|
||||
`Domain` is a value from this list; add a row to introduce a domain, so a
|
||||
reviewer can see at once which professional vocabulary a document uses.
|
||||
|
||||
| Domain | Meaning |
|
||||
| --- | --- |
|
||||
| it | Software and IT |
|
||||
| medical | Healthcare and medical devices |
|
||||
| construction | Construction and civil engineering |
|
||||
|
||||
| Artifact types | Register | Written in the PO language |
|
||||
| --- | --- | --- |
|
||||
| BC, KPI, PP, MIL | IT Executive English | Yes |
|
||||
| SA, BMC, BPMN, UCD, US, UC, SSD, DM, RA, GOV, DICT | IT Professional English | Yes |
|
||||
| OC, SD, DCD, ERD, TP, AD, ADR, TM, RC, QC, source code | IT Professional English | No |
|
||||
|
||||
## Notes
|
||||
|
||||
- "Next Available Version" is the zero-padded (3-digit) version to use the
|
||||
*next* time a new document of that type is created. Increment it only when
|
||||
a brand-new document is created, not when an existing document's
|
||||
`## Version History` gets a row.
|
||||
- `ADR` uses 4 digits (`0001`); `RC` is sequential across all artifact types.
|
||||
# Artifact Registry
|
||||
|
||||
This project's artifact state. Types, short names and `CrossReference
|
||||
Candidates` come from the framework catalog
|
||||
(`framework/registry/artifact-catalog.md`); this file only records where
|
||||
each document lives in *this* project and the next version to use.
|
||||
|
||||
Delete rows for types you don't use. Add a row the first time you create a
|
||||
document of a type. `Primary File` may contain a glob (e.g.
|
||||
`docs/use-cases/uc-*/uc.md`); `framework/scripts/find-crossreferences.sh` reads it.
|
||||
|
||||
| Short Name | Artifact Type | Primary File | Next Available Version |
|
||||
| --- | --- | --- | --- |
|
||||
| BC | Business Case | docs/business-case.md | 002 |
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/mil-*.md | 008 |
|
||||
| GOV | Governance / ARB Workflow | docs/sqa/governance.md | 002 |
|
||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 018 |
|
||||
|
||||
## Languages
|
||||
|
||||
Set the PO language when the project starts; `project-planning` asks for it
|
||||
if it is missing. An artifact of a type marked "Written in the PO language"
|
||||
exists once, in that language, under its normal name (`business-case.md`); the
|
||||
`artifact` skill ("One file per artifact") has the rule.
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| PO language | en |
|
||||
| PO domain | it |
|
||||
| High-level register | IT Executive English |
|
||||
| Technical register | IT Professional English |
|
||||
| Wiki languages | en, da, ms (`framework/process/wiki.md`) |
|
||||
|
||||
Every artifact of a type marked "Yes" below states its language and domain in
|
||||
its `Language` and `Domain` Metadata rows; `new-artifact.sh` fills them from
|
||||
`PO language` and `PO domain`. `Language` is a BCP 47 code (`da`, `en`).
|
||||
`Domain` is a value from this list; add a row to introduce a domain, so a
|
||||
reviewer can see at once which professional vocabulary a document uses.
|
||||
|
||||
| Domain | Meaning |
|
||||
| --- | --- |
|
||||
| it | Software and IT |
|
||||
| medical | Healthcare and medical devices |
|
||||
| construction | Construction and civil engineering |
|
||||
|
||||
| Artifact types | Register | Written in the PO language |
|
||||
| --- | --- | --- |
|
||||
| BC, KPI, PP, MIL | IT Executive English | Yes |
|
||||
| SA, BMC, BPMN, UCD, US, UC, SSD, DM, RA, GOV, DICT | IT Professional English | Yes |
|
||||
| OC, SD, DCD, ERD, TP, AD, ADR, TM, RC, QC, source code | IT Professional English | No |
|
||||
|
||||
## Notes
|
||||
|
||||
- "Next Available Version" is the zero-padded (3-digit) version to use the
|
||||
*next* time a new document of that type is created. Increment it only when
|
||||
a brand-new document is created, not when an existing document's
|
||||
`## Version History` gets a row.
|
||||
- `ADR` uses 4 digits (`0001`); `RC` is sequential across all artifact types.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Review of the publish-wiki workflow of MIL-007
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-017 |
|
||||
| CrossReference | [MIL-007], [QC-WF-001], [QC-YML-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-28 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [MIL-007] (the workflow `.gitea/workflows/publish-wiki.yml`, task 5)
|
||||
- Checklist used: [QC-WF-001] and [QC-YML-001]
|
||||
- Scope: full review
|
||||
- Language and domain: n/a
|
||||
- Language reviewer: none
|
||||
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||
|
||||
## Checklist Results
|
||||
|
||||
Workflow checklist (`QC-WF-001`):
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The file meets the YAML checklist ([QC-YML-001]) | Pass | Passes the YAML checklist below. |
|
||||
| 2 | The file is in `.gitea/workflows/` or `.github/workflows/` with the extension `.yml` or `.yaml`, and the same workflow does not exist in both directories | Pass | File is .gitea/workflows/publish-wiki.yml; no copy in .github/workflows. |
|
||||
| 3 | The file declares `name`, `on` and `jobs`; `on` lists explicit events and, where it matters, `branches` or `paths` | Pass | name, on (push, branches main, paths docs/wiki/**) and jobs are declared. |
|
||||
| 4 | A header comment states the purpose, the triggers, the inputs, the secrets it needs, what it changes outside the repository, and the decision or milestone it implements | Pass | Header states purpose, trigger, inputs, secret, outside effect and decision MIL-007. |
|
||||
| 5 | Every job and step has a descriptive `name`, unique within its job | Pass | Job name `Publish wiki pages`. |
|
||||
| 6 | The file passes `actionlint` (or the project's workflow validation test) and the tool version is stated | Pass | `actionlint` 1.7.12 reports no finding. |
|
||||
| 7 | Every job declares `permissions` explicitly with the least access that works (`contents: read` or none by default); write access only on the job that needs it | Pass | Workflow level `permissions: contents: read`; the reusable workflow uses the wiki token secret. |
|
||||
| 8 | Every action used is pinned to a full commit SHA or an exact release tag, never to a branch; a moving major tag (`@v1`) is accepted only for an action published by the same organisation under a stated release policy | Pass | `@v1` of TirSystem/git-workflows, the organization that also publishes the framework, under its release policy for the major tag (same as the framework template). |
|
||||
| 9 | No secret is written in the file; a secret is read as `secrets.NAME`, passed to a step through `env:`, never placed on a command line or echoed | Pass | Only `secrets.TOKEN_FOR_GITEA_WIKI` is referenced, passed by name. |
|
||||
| 10 | Untrusted text (`inputs.*`, event fields such as a title, branch name or label, step outputs derived from them) is never interpolated into a `run:` script; it is passed through `env:` and used as a quoted variable | Pass | No untrusted text is used in any script; the workflow has no run: step. |
|
||||
| 11 | A trigger that gives a run secrets or a write token on code from a fork or an untrusted branch (`pull_request_target`, `workflow_run`) does not check out or execute that code | Pass | Trigger is `push` to main only; no pull_request_target or workflow_run. |
|
||||
| 12 | A workflow that changes state outside the repository (release, tag, publish, sync, deploy) is started by hand or by a protected branch, and a manual run defaults to a dry run with an explicit input to apply | Pass | Started only by a push to the protected branch main; the manual trigger of the template was removed so no unplanned run changes the wiki. |
|
||||
| 13 | The token the workflow uses has only the scope the job needs, and where the host's built-in token is enough it is used instead of a personal token | Pass | The token scope is write:repository for the wiki only (header). |
|
||||
| 14 | Every job sets `runs-on` to an explicit label and `timeout-minutes` | N-A | The job only calls a reusable workflow; `runs-on` and `timeout-minutes` are set inside it (a calling job cannot set them). |
|
||||
| 15 | `concurrency` is set where two runs would conflict: a check cancels the older run (`cancel-in-progress: true`); a publishing job queues (`cancel-in-progress: false`) | Pass | `concurrency` group publish-wiki with `cancel-in-progress: false`: publishing runs queue. |
|
||||
| 16 | Triggers are filtered (`branches`, `paths`, `types`) so the workflow runs only for the changes it concerns | Pass | Filtered by branch main and paths docs/wiki/**. |
|
||||
| 17 | Tools and dependencies are installed at pinned versions; a version is a workflow input with a default or a value in one place | N-A | The workflow installs no tools; the called workflow is pinned by @v1. |
|
||||
| 18 | A dependency cache is used where the install is slow enough to matter | N-A | Nothing is installed. |
|
||||
| 19 | A failing step names the tool, the file and the reason in its log (annotation or `::error::` line) | N-A | No step of its own. |
|
||||
| 20 | A `run:` block is within the limit on embedded program text of criterion 7 of [QC-YML-001]; a longer script is a file (`tools/`, or a shared repository fetched at a pinned ref) that the step calls with one command, and the workflow passes its settings through `env:` | N-A | No run: block. |
|
||||
| 21 | Logic used by more than one workflow or repository is a reusable workflow (`workflow_call`) or a composite action; each input has a `description`, a `type` and a `default` where one is sensible | N-A | This file is the caller of a reusable workflow, not a new reusable unit. |
|
||||
| 22 | Every workflow feature used is verified on the instance that runs it, and the host and version are recorded; a feature the target hosts differ on is noted in the header | Pending | Not verified on the instance: the secret TOKEN_FOR_GITEA_WIKI and a runner are not set up by the author of this review; S01 verifies the first run and records the Gitea version. |
|
||||
| 23 | A reusable workflow states, in its header, how a caller pins it and the script it fetches, so the workflow and its script are the same release | Pass | Header names the called workflow and its major tag. |
|
||||
| 24 | A static test of the repository's workflows exists and runs on every pull request: valid YAML, required keys, explicit permissions and runner, no inline script above the limit | N-A | No static workflow test exists in this project; actionlint and yamllint were run by hand. Optional. |
|
||||
|
||||
YAML checklist (`QC-YML-001`):
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The file parses with a safe YAML loader and has no duplicate keys in any mapping | Pass | yamllint 1.38.0 parses the file; no duplicate keys. |
|
||||
| 2 | Indentation is 2 spaces everywhere, never tabs; lines end with LF; the file ends with one newline and has no trailing spaces | Pass | 2 spaces, LF, one final newline (yamllint). |
|
||||
| 3 | Passes `yamllint` (or the project's YAML linter) with its configuration committed next to the code and no unexplained `disable` comments | Pass | yamllint 1.38.0 with `.yamllint` committed; no disable comments. |
|
||||
| 4 | A scalar that YAML would coerce to another type is quoted: `yes`, `no`, `on`, `off`, `null`, `~`, numbers with a leading zero, versions such as `1.10`, and values that contain `: ` or ` #` | Pass | `on` is not an issue for the loader; `prune` is quoted as a string. |
|
||||
| 5 | Only safe, plain types are used: no language-specific tags (`!!python/object`, `!ruby/object`) and no `!include`-style extension unless the consuming tool documents it | Pass | No tags. |
|
||||
| 6 | No secret is written in the file: no password, token, key or private connection string; a secret is referenced by name and supplied by the consuming tool | Pass | No secret is written in the file. |
|
||||
| 7 | Embedded program text (shell, Python, SQL, `jq`, and so on) in a value is at most five lines; a longer one is moved to a file of its own, in its own language, and the YAML calls it | Pass | No embedded program text. |
|
||||
| 8 | A block of keys or values repeated in the file or across files is defined once (an anchor, a template, an include or a reusable unit the consumer supports) | Pass | Nothing repeated. |
|
||||
| 9 | Anchors, aliases and merge keys (`&`, `*`, `<<`) are used only where the consuming tool is known to support them | Pass | No anchors or aliases. |
|
||||
| 10 | The file validates against the schema of the consuming tool where one exists, and the schema or tool version is stated | Pass | actionlint validates the workflow schema (1.7.12). |
|
||||
| 11 | A header comment states what the file is for, which tool reads it, and where its decision or design is recorded | Pass | Header states purpose, tool and the decision. |
|
||||
| 12 | Key names follow the convention of the consuming tool; one case style is used within the file (kebab-case or snake_case, never mixed) | Pass | Keys follow the Gitea Actions convention (snake_case in `with`, lowercase elsewhere). |
|
||||
| 13 | Multi-line strings use a block scalar (`\|` or `>`) and state the chomping indicator (`\|-`, `\|+`) where the trailing newline matters | Pass | No multi-line scalars. |
|
||||
| 14 | The file name is lowercase kebab-case with one extension style (`.yml` or `.yaml`) used throughout the repository | Pass | `publish-wiki.yml`, lowercase kebab-case, `.yml`. |
|
||||
| 15 | A change is exercised by a check that runs without the real target: a parse and schema test, or a dry run of the consuming tool | Pass | Exercised by actionlint and yamllint without the real target. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go-with-conditions — every Mandatory criterion passes except criterion 22 of QC-WF-001, which stays `Pending`: only a first run on the Gitea instance can verify it. The workflow also only works once the organization secret `TOKEN_FOR_GITEA_WIKI` exists and a runner is registered.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Create the organization secret `TOKEN_FOR_GITEA_WIKI` and register a runner | S01 | 2026-11-30 |
|
||||
| Check the first run of the workflow on the Gitea instance and record the Gitea version (criterion 22) | S01 | 2026-11-30 |
|
||||
|
||||
---
|
||||
|
||||
[MIL-007]: ../../milestones/mil-007-wiki-and-release.md
|
||||
[QC-WF-001]: ../../../framework/qc/code/qc-workflow.md
|
||||
[QC-YML-001]: ../../../framework/qc/code/qc-yaml.md
|
||||
@@ -31,7 +31,7 @@ Tracks backward and forward links between artifact instances so that the traceab
|
||||
| [MIL-004] | MIL | en | it | [MIL-003] | [MIL-005] | [RC-008], [RC-014] |
|
||||
| [MIL-005] | MIL | en | it | [MIL-004] | [MIL-006] | [RC-009], [RC-015] |
|
||||
| [MIL-006] | MIL | en | it | [MIL-005] | [MIL-007] | [RC-010], [RC-016] |
|
||||
| [MIL-007] | MIL | en | it | [MIL-006] | - | [RC-011] |
|
||||
| [MIL-007] | MIL | en | it | [MIL-006] | - | [RC-011], [RC-017] |
|
||||
| [TM-001] | TM | - | - | [BC-001] | - | - |
|
||||
|
||||
## Concern Coverage
|
||||
@@ -86,3 +86,4 @@ Tracks backward and forward links between artifact instances so that the traceab
|
||||
[RC-014]: ./reviews/rc-014-mil-004-code.md
|
||||
[RC-015]: ./reviews/rc-015-mil-005-code.md
|
||||
[RC-016]: ./reviews/rc-016-mil-006-code.md
|
||||
[RC-017]: ./reviews/rc-017-mil-007-workflow.md
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Musical Time Machine
|
||||
|
||||
🌐 **English** · [Dansk](Home-da) · [Bahasa Melayu](Home-ms)
|
||||
|
||||
En løsning på dag 46 i Udemys *100 Days of Code: The Complete Python Pro Bootcamp*. Skriv en dato, så scraper programmet Billboard Hot 100 for den dag (scraping betyder, at et program automatisk henter og læser data fra en hjemmeside) og bygger en privat YouTube Music-playliste af sangene, så du kan høre musikken fra en dag, der betyder noget for dig.
|
||||
|
||||
Wikien følger forelæsningens fire trin. Hver side viser, hvad trinnet gør, et skærmbillede og **Det lærte vi**: den teknik, trinnet lærer os.
|
||||
|
||||
| Side | Læs den, når du vil |
|
||||
| --- | --- |
|
||||
| [Setup og kørsel](Setup-and-Run-da) | Installere projektet i et `.venv`, oprette `browser.json` og køre testene |
|
||||
| [Trin 1 - Scrape Billboard Hot 100](Step-1-Scrape-the-Billboard-da) | Se, hvordan `requests` og `BeautifulSoup` gør en hjemmeside til en Python-liste |
|
||||
| [Trin 2 - YouTube Music-godkendelse](Step-2-YouTube-Music-Authentication-da) | Se, hvordan `ytmusicapi` låner din browsers login |
|
||||
| [Trin 3 - Opret playlisten](Step-3-Create-the-Playlist-da) | Se, hvordan man opretter noget én gang og aldrig to |
|
||||
| [Trin 4 - Søg efter og tilføj sange](Step-4-Search-and-Add-Songs-da) | Se, hvordan en lang løkke kører videre, når ét element fejler |
|
||||
|
||||
## Overblik
|
||||
|
||||
| Lag | Hvad det indeholder |
|
||||
| --- | --- |
|
||||
| `domain` | `Song`, `Playlist`, datoreglerne og fejlene |
|
||||
| `application` | Use cases (`scrape_titles`, `create_playlist`, `add_songs`) og de porte, de har brug for |
|
||||
| `infrastructure` | Bakeboard-scraperen og `ytmusicapi`-adapteren |
|
||||
| `presentation` | Kommandolinjen, som forbinder det hele |
|
||||
|
||||
Alle konstanter ligger i `constants.py`. Testene bruger en gemt kopi af hitlistesiden og en falsk YouTube Music-tjeneste, så de aldrig har brug for netværket eller en konto.
|
||||
|
||||
## Skærmbilleder
|
||||
|
||||
Skærmbillederne af trin 1 er fra en rigtig kørsel. Skærmbillederne af trin 2 til 4 viser kørslen uden `browser.json` og testene med en falsk YouTube Music-tjeneste; rigtige skærmbilleder tilføjes, når forfatteren har oprettet `browser.json`.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Musical Time Machine
|
||||
|
||||
🌐 **English** · [Dansk](Home-da) · [Bahasa Melayu](Home-ms)
|
||||
|
||||
Penyelesaian untuk hari ke-46 kursus Udemy *100 Days of Code: The Complete Python Pro Bootcamp*. Taip satu tarikh, dan program ini mengikis web (scraping) Billboard Hot 100 pada hari itu, lalu membina senarai main YouTube Music yang peribadi daripada lagu-lagu tersebut, supaya anda dapat mendengar muzik pada hari yang bermakna bagi anda.
|
||||
|
||||
Wiki ini mengikuti empat langkah dalam kuliah. Setiap halaman menunjukkan apa yang dilakukan oleh langkah itu, satu tangkapan skrin, dan **apa yang kita pelajari**: teknik yang diajar oleh langkah tersebut.
|
||||
|
||||
| Halaman | Baca apabila anda mahu |
|
||||
| --- | --- |
|
||||
| [Persediaan dan Menjalankan](Setup-and-Run-ms) | Memasang projek dalam `.venv`, mencipta `browser.json` dan menjalankan ujian |
|
||||
| [Langkah 1 - Mengikis Billboard Hot 100](Step-1-Scrape-the-Billboard-ms) | Melihat bagaimana `requests` dan `BeautifulSoup` menukar laman web menjadi senarai Python |
|
||||
| [Langkah 2 - Pengesahan YouTube Music](Step-2-YouTube-Music-Authentication-ms) | Melihat bagaimana `ytmusicapi` meminjam log masuk pelayar anda |
|
||||
| [Langkah 3 - Mencipta Senarai Main](Step-3-Create-the-Playlist-ms) | Melihat cara mencipta sesuatu sekali sahaja dan tidak pernah dua kali |
|
||||
| [Langkah 4 - Mencari dan Menambah Lagu](Step-4-Search-and-Add-Songs-ms) | Melihat bagaimana gelung yang panjang terus berjalan apabila satu item gagal |
|
||||
|
||||
## Gambaran keseluruhan
|
||||
|
||||
| Lapisan | Kandungannya |
|
||||
| --- | --- |
|
||||
| `domain` | `Song`, `Playlist`, peraturan tarikh dan ralat |
|
||||
| `application` | Kes penggunaan (`scrape_titles`, `create_playlist`, `add_songs`) dan port yang diperlukannya |
|
||||
| `infrastructure` | Pengikis Bakeboard dan penyesuai `ytmusicapi` |
|
||||
| `presentation` | Baris perintah, yang menghubungkan segala-galanya |
|
||||
|
||||
Setiap pemalar berada dalam `constants.py`. Ujian menggunakan salinan tersimpan bagi laman carta dan YouTube Music tiruan, jadi ia tidak memerlukan rangkaian atau akaun.
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||
Tangkapan skrin langkah 1 ialah larian sebenar. Tangkapan skrin langkah 2 hingga 4 menunjukkan larian tanpa `browser.json` dan ujian dengan YouTube Music tiruan; tangkapan skrin langsung akan ditambah apabila penulis telah mencipta `browser.json`.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Musical Time Machine
|
||||
|
||||
🌐 **English** · [Dansk](Home-da) · [Bahasa Melayu](Home-ms)
|
||||
|
||||
A solution to day 46 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*. Type a date, and the program scrapes the Billboard Hot 100 of that day and builds a private YouTube Music playlist from the songs, so you can listen to the music of a day that matters to you.
|
||||
|
||||
The wiki follows the four steps of the lecture. Each page shows what the step does, a screenshot, and **what we gained**: the technique the step teaches.
|
||||
|
||||
| Page | Read it when you want to |
|
||||
| --- | --- |
|
||||
| [Setup and Run](Setup-and-Run) | Install the project in a `.venv`, create `browser.json` and run the tests |
|
||||
| [Step 1 - Scrape the Billboard Hot 100](Step-1-Scrape-the-Billboard) | See how `requests` and `BeautifulSoup` turn a web page into a Python list |
|
||||
| [Step 2 - YouTube Music Authentication](Step-2-YouTube-Music-Authentication) | See how `ytmusicapi` borrows your browser's login |
|
||||
| [Step 3 - Create the Playlist](Step-3-Create-the-Playlist) | See how to create something once and never twice |
|
||||
| [Step 4 - Search and Add Songs](Step-4-Search-and-Add-Songs) | See how a long loop keeps going when one item fails |
|
||||
|
||||
## Overview
|
||||
|
||||
| Layer | What it holds |
|
||||
| --- | --- |
|
||||
| `domain` | `Song`, `Playlist`, the date rules and the errors |
|
||||
| `application` | The use cases (`scrape_titles`, `create_playlist`, `add_songs`) and the ports they need |
|
||||
| `infrastructure` | The Bakeboard scraper and the `ytmusicapi` adapter |
|
||||
| `presentation` | The command line, which connects everything |
|
||||
|
||||
Every constant is in `constants.py`. The tests use a stored copy of the chart page and a fake YouTube Music, so they never need the network or an account.
|
||||
|
||||
## Screenshots
|
||||
|
||||
The screenshots of step 1 are a real run. The screenshots of steps 2 to 4 show the run without `browser.json` and the tests with a fake YouTube Music; live screenshots are added when the author has created `browser.json`.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Setup og kørsel
|
||||
|
||||
🌐 **English** · [Dansk](Setup-and-Run-da) · [Bahasa Melayu](Setup-and-Run-ms)
|
||||
|
||||
## Hvad du skal bruge
|
||||
|
||||
- Python 3.13 eller nyere.
|
||||
- Til trin 2 til 4: en Google-konto og Firefox.
|
||||
|
||||
## Opret det virtuelle miljø
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
Aktivér det. I Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
På Linux og macOS:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Opgradér `pip` og installér projektet:
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Opret `browser.json`
|
||||
|
||||
`ytmusicapi` har ingen API-nøgle. Biblioteket genbruger din browsers login, så du kopierer anmodningshovederne (request headers) fra en logget ind session én gang.
|
||||
|
||||
1. Åbn Firefox og gå til <https://music.youtube.com>, logget ind.
|
||||
2. Åbn udviklerværktøjerne (`Ctrl+Shift+I`, på macOS `Cmd+Option+I`) og vælg **Network**.
|
||||
3. Skriv `browse` i filteret, vælg en vellykket `browse?...`-anmodning, højreklik på den og vælg *Copy Value* og derefter *Copy Request Headers*.
|
||||
4. Kør `ytmusicapi browser` i projektmappen og indsæt hovederne. Afslut med `Ctrl+D` (`Ctrl+Z` og Enter på Windows). På macOS kører du `pbpaste | ytmusicapi browser`.
|
||||
|
||||
Filen indeholder dine cookies: den ignoreres af git og må aldrig deles. Cookies udløber efter et stykke tid; gentag trinnene for at få en ny fil.
|
||||
|
||||
## Kørsel
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only # step 1: print the chart
|
||||
python -m musical_time_machine --check-login # step 2: count your playlists
|
||||
python -m musical_time_machine --create-playlist # step 3: create the playlist
|
||||
python -m musical_time_machine # step 4: the full program
|
||||
```
|
||||
|
||||
Datoformatet er `YYYY-MM-DD`; brug en dato, som Bakeboard har, for eksempel `2026-04-18`.
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Testene kræver hverken netværk eller konto.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Persediaan dan Menjalankan
|
||||
|
||||
🌐 **English** · [Dansk](Setup-and-Run-da) · [Bahasa Melayu](Setup-and-Run-ms)
|
||||
|
||||
## Apa yang anda perlukan
|
||||
|
||||
- Python 3.13 atau lebih baharu.
|
||||
- Untuk langkah 2 hingga 4: akaun Google dan Firefox.
|
||||
|
||||
## Cipta persekitaran maya
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
Aktifkannya. Pada Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
Pada Linux dan macOS:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Naik taraf `pip` dan pasang projek:
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Cipta `browser.json`
|
||||
|
||||
`ytmusicapi` tidak mempunyai kunci API. Ia menggunakan semula log masuk pelayar anda, jadi anda menyalin pengepala permintaan bagi sesi yang telah log masuk sekali sahaja.
|
||||
|
||||
1. Buka Firefox dan pergi ke <https://music.youtube.com>, dalam keadaan telah log masuk.
|
||||
2. Buka alat pembangun (`Ctrl+Shift+I`, pada macOS `Cmd+Option+I`) dan pilih **Network**.
|
||||
3. Taip `browse` dalam penapis, pilih satu permintaan `browse?...` yang berjaya, klik kanan padanya dan pilih *Copy Value*, kemudian *Copy Request Headers*.
|
||||
4. Jalankan `ytmusicapi browser` dalam folder projek dan tampal pengepala tersebut. Akhiri dengan `Ctrl+D` (`Ctrl+Z` dan Enter pada Windows). Pada macOS jalankan `pbpaste | ytmusicapi browser`.
|
||||
|
||||
Fail ini mengandungi kuki anda: ia diabaikan oleh git dan tidak boleh dikongsi sama sekali. Kuki akan tamat tempoh selepas beberapa lama; ulangi langkah-langkah ini untuk mendapatkan fail yang baharu.
|
||||
|
||||
## Jalankan
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only # step 1: print the chart
|
||||
python -m musical_time_machine --check-login # step 2: count your playlists
|
||||
python -m musical_time_machine --create-playlist # step 3: create the playlist
|
||||
python -m musical_time_machine # step 4: the full program
|
||||
```
|
||||
|
||||
Format tarikh ialah `YYYY-MM-DD`; gunakan tarikh yang ada pada Bakeboard, contohnya `2026-04-18`.
|
||||
|
||||
## Uji
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Ujian tidak memerlukan rangkaian dan tidak memerlukan akaun.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Setup and Run
|
||||
|
||||
🌐 **English** · [Dansk](Setup-and-Run-da) · [Bahasa Melayu](Setup-and-Run-ms)
|
||||
|
||||
## What you need
|
||||
|
||||
- Python 3.13 or later.
|
||||
- For steps 2 to 4: a Google account and Firefox.
|
||||
|
||||
## Create the virtual environment
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
Activate it. On Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
On Linux and macOS:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Upgrade `pip` and install the project:
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Create `browser.json`
|
||||
|
||||
`ytmusicapi` has no API key. It reuses the login of your browser, so you copy the request headers of a logged-in session once.
|
||||
|
||||
1. Open Firefox and go to <https://music.youtube.com>, logged in.
|
||||
2. Open the developer tools (`Ctrl+Shift+I`, on macOS `Cmd+Option+I`) and choose **Network**.
|
||||
3. Type `browse` in the filter, pick a successful `browse?...` request, right-click it and choose *Copy Value*, then *Copy Request Headers*.
|
||||
4. Run `ytmusicapi browser` in the project folder and paste the headers. Finish with `Ctrl+D` (`Ctrl+Z` and Enter on Windows). On macOS run `pbpaste | ytmusicapi browser`.
|
||||
|
||||
The file contains your cookies: it is ignored by git and must never be shared. The cookies expire after a while; repeat the steps for a fresh file.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only # step 1: print the chart
|
||||
python -m musical_time_machine --check-login # step 2: count your playlists
|
||||
python -m musical_time_machine --create-playlist # step 3: create the playlist
|
||||
python -m musical_time_machine # step 4: the full program
|
||||
```
|
||||
|
||||
The date format is `YYYY-MM-DD`; use a date that Bakeboard has, for example `2026-04-18`.
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
The tests need no network and no account.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Trin 1 - Scrape Billboard Hot 100
|
||||
|
||||
🌐 **English** · [Dansk](Step-1-Scrape-the-Billboard-da) · [Bahasa Melayu](Step-1-Scrape-the-Billboard-ms)
|
||||
|
||||
## Hvad trinnet gør
|
||||
|
||||
Programmet beder om en dato i formatet `YYYY-MM-DD`, henter `https://appbrewery.github.io/bakeboard-hot-100/<date>/` og læser de 100 sange ud af HTML'en. Bakeboard er kursets kopi af Billboard Hot 100, fordi den rigtige side kan ændre sig eller få en betalingsmur når som helst.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only
|
||||
```
|
||||
|
||||
Bakeboard-siden for 2026-04-18:
|
||||
|
||||

|
||||
|
||||
Programmets output (gengivet ud fra teksten fra en rigtig kørsel, ikke et foto af et terminalvindue):
|
||||
|
||||

|
||||
|
||||
## Det lærte vi
|
||||
|
||||
- **At læse en hjemmeside som data.** `BeautifulSoup` gør HTML til et træ, og en CSS-selektor (`div.chart-entry`, `h3.chart-entry__title`) udpeger de dele, vi vil have. Vi fandt selektorerne ved først at se på sidens markup.
|
||||
- **At være en høflig klient.** En anmodning har hoveder (headers); `User-Agent` fortæller serveren, hvilken slags klient der spørger. En `timeout` og `raise_for_status()` får en langsom eller manglende side til at fejle tydeligt i stedet for at hænge.
|
||||
- **At validere input ved grænsen.** `parse_chart_date` accepterer kun rigtige `YYYY-MM-DD`-datoer, så resten af programmet aldrig ser en ugyldig dato.
|
||||
- **At teste uden netværk.** Hitlistesiden er gemt under `tests/fixtures/`, og scraperen tager sin sidehenter som parameter, så testene kontrollerer 100 sange uden en eneste anmodning.
|
||||
- **At samle viden ét sted.** URL, selektorer og beskeder ligger i `constants.py`; hvis Bakeboard ændrer sit markup, er det én fil, der ændres.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Langkah 1 - Mengikis Billboard Hot 100
|
||||
|
||||
🌐 **English** · [Dansk](Step-1-Scrape-the-Billboard-da) · [Bahasa Melayu](Step-1-Scrape-the-Billboard-ms)
|
||||
|
||||
## Apa yang dilakukan oleh langkah ini
|
||||
|
||||
Program meminta satu tarikh dalam format `YYYY-MM-DD`, membuat permintaan ke `https://appbrewery.github.io/bakeboard-hot-100/<date>/` dan membaca 100 lagu daripada HTML. Bakeboard ialah klon Billboard Hot 100 yang disediakan kursus, kerana laman sebenar boleh berubah atau menambah tembok bayaran pada bila-bila masa.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only
|
||||
```
|
||||
|
||||
Laman Bakeboard bagi 2026-04-18:
|
||||
|
||||

|
||||
|
||||
Output program (dipaparkan daripada teks larian sebenar, bukan gambar tetingkap terminal):
|
||||
|
||||

|
||||
|
||||
## Apa yang kita pelajari
|
||||
|
||||
- **Membaca laman web sebagai data.** `BeautifulSoup` menukar HTML menjadi pepohon, dan pemilih CSS (`div.chart-entry`, `h3.chart-entry__title`) memilih bahagian yang kita mahu. Kita menemui pemilih tersebut dengan melihat penanda halaman terlebih dahulu.
|
||||
- **Menjadi klien yang sopan.** Sesuatu permintaan membawa pengepala; `User-Agent` memberitahu pelayan jenis klien yang meminta. `timeout` dan `raise_for_status()` menjadikan halaman yang perlahan atau tiada gagal dengan jelas dan bukannya tergantung.
|
||||
- **Mengesahkan input di sempadan.** `parse_chart_date` hanya menerima tarikh `YYYY-MM-DD` yang sebenar, jadi bahagian lain program tidak pernah menerima tarikh yang salah.
|
||||
- **Menguji tanpa rangkaian.** Laman carta disimpan di bawah `tests/fixtures/`, dan pengikis menerima pengambil halamannya sebagai parameter, jadi ujian memeriksa 100 lagu tanpa satu permintaan pun.
|
||||
- **Menyimpan pengetahuan di satu tempat.** URL, pemilih dan mesej berada dalam `constants.py`; jika Bakeboard mengubah penandanya, hanya satu fail yang berubah.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Step 1 - Scrape the Billboard Hot 100
|
||||
|
||||
🌐 **English** · [Dansk](Step-1-Scrape-the-Billboard-da) · [Bahasa Melayu](Step-1-Scrape-the-Billboard-ms)
|
||||
|
||||
## What the step does
|
||||
|
||||
The program asks for a date in the format `YYYY-MM-DD`, requests `https://appbrewery.github.io/bakeboard-hot-100/<date>/` and reads the 100 songs out of the HTML. Bakeboard is the course's clone of the Billboard Hot 100, because the real site can change or add a paywall at any time.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --list-only
|
||||
```
|
||||
|
||||
The Bakeboard page of 2026-04-18:
|
||||
|
||||

|
||||
|
||||
The output of the program (rendered from the text of a real run, not a photo of a terminal window):
|
||||
|
||||

|
||||
|
||||
## What we gained
|
||||
|
||||
- **Reading a web page as data.** `BeautifulSoup` turns HTML into a tree, and a CSS selector (`div.chart-entry`, `h3.chart-entry__title`) picks the parts we want. We found the selectors by looking at the page's markup first.
|
||||
- **Being a polite client.** A request carries headers; the `User-Agent` tells the server what kind of client asks. A `timeout` and `raise_for_status()` make a slow or missing page fail clearly instead of hanging.
|
||||
- **Validating input at the edge.** `parse_chart_date` accepts only real `YYYY-MM-DD` dates, so the rest of the program never sees a bad date.
|
||||
- **Testing without the network.** The chart page is stored under `tests/fixtures/`, and the scraper takes its page fetcher as a parameter, so the tests check 100 songs without a single request.
|
||||
- **Keeping the knowledge in one place.** URL, selectors and messages live in `constants.py`; if Bakeboard changes its markup, one file changes.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Trin 2 - YouTube Music-godkendelse
|
||||
|
||||
🌐 **English** · [Dansk](Step-2-YouTube-Music-Authentication-da) · [Bahasa Melayu](Step-2-YouTube-Music-Authentication-ms)
|
||||
|
||||
## Hvad trinnet gør
|
||||
|
||||
YouTube Music har ingen enkel API-nøgle. Biblioteket `ytmusicapi` virker ved at låne din browsers login: du kopierer anmodningshovederne fra en logget ind Firefox-session én gang (se [Setup og kørsel](Setup-and-Run-da)), og `ytmusicapi browser` gør dem til `browser.json`. Programmet åbner derefter `YTMusic("browser.json")` og tæller afspilningslisterne i dit bibliotek.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --check-login
|
||||
```
|
||||
|
||||
Ved succes udskriver det `Found N playlists in your library.` Uden en gyldig `browser.json` udskriver det trinnene til fornyelse og afslutter med kode 1 i stedet for at vise et stack trace.
|
||||
|
||||
Skærmbilledet viser kørslen **uden** `browser.json` og de tests, der bruger en falsk YouTube Music-tjeneste. Et rigtigt skærmbillede tilføjes, når `browser.json` findes.
|
||||
|
||||

|
||||
|
||||
## Det lærte vi
|
||||
|
||||
- **Hvordan cookie-baseret godkendelse virker.** Din browser beviser, hvem du er, med cookies og hoveder på hver anmodning; ved at kopiere dem kan et script optræde som den session. Det er også derfor, `browser.json` er en hemmelighed, ignoreres af git og udløber efter et stykke tid.
|
||||
- **At skjule et bibliotek bag en port.** Programmet taler med interfacet `MusicService`; kun `infrastructure/ytmusic.py` importerer `ytmusicapi`. Testene bruger en falsk tjeneste, og en anden musiktjeneste kunne tage dens plads.
|
||||
- **At fejle hjælpsomt.** En manglende eller udløbet fil bliver til en tydelig besked, der siger, hvordan den fornyes, fordi den fejl, en bruger ser, er en del af produktet.
|
||||
- **En doven forbindelse.** Klienten oprettes ved første brug, så opbygningen af tjenesten aldrig rører netværket.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Langkah 2 - Pengesahan YouTube Music
|
||||
|
||||
🌐 **English** · [Dansk](Step-2-YouTube-Music-Authentication-da) · [Bahasa Melayu](Step-2-YouTube-Music-Authentication-ms)
|
||||
|
||||
## Apa yang dilakukan oleh langkah ini
|
||||
|
||||
YouTube Music tidak mempunyai kunci API yang mudah. Pustaka `ytmusicapi` berfungsi dengan meminjam log masuk pelayar anda: anda menyalin pengepala permintaan bagi sesi Firefox yang telah log masuk sekali sahaja (lihat [Persediaan dan Menjalankan](Setup-and-Run-ms)), dan `ytmusicapi browser` menukarnya menjadi `browser.json`. Program kemudian membuka `YTMusic("browser.json")` dan mengira senarai main dalam pustaka anda.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --check-login
|
||||
```
|
||||
|
||||
Jika berjaya, ia mencetak `Found N playlists in your library.` Tanpa `browser.json` yang sah, ia mencetak langkah pembaharuan dan keluar dengan kod 1, dan bukannya jejak tindanan.
|
||||
|
||||
Tangkapan skrin menunjukkan larian **tanpa** `browser.json` dan ujian yang menggunakan YouTube Music tiruan. Tangkapan skrin langsung akan ditambah apabila `browser.json` wujud.
|
||||
|
||||

|
||||
|
||||
## Apa yang kita pelajari
|
||||
|
||||
- **Bagaimana pengesahan berasaskan kuki berfungsi.** Pelayar anda membuktikan siapa anda dengan kuki dan pengepala pada setiap permintaan; menyalinnya membolehkan skrip bertindak sebagai sesi itu. Itulah juga sebabnya `browser.json` ialah rahsia, diabaikan oleh git dan tamat tempoh selepas beberapa lama.
|
||||
- **Menyembunyikan pustaka di sebalik port.** Program berinteraksi dengan antara muka `MusicService`; hanya `infrastructure/ytmusic.py` yang mengimport `ytmusicapi`. Ujian menggunakan perkhidmatan tiruan, dan perkhidmatan muzik lain boleh menggantikannya.
|
||||
- **Gagal dengan membantu.** Fail yang hilang atau tamat tempoh menjadi mesej yang jelas yang menerangkan cara memperbaharuinya, kerana ralat yang dilihat pengguna ialah sebahagian daripada produk.
|
||||
- **Sambungan malas.** Klien dicipta pada penggunaan pertama, jadi membina perkhidmatan tidak pernah menyentuh rangkaian.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Step 2 - YouTube Music Authentication
|
||||
|
||||
🌐 **English** · [Dansk](Step-2-YouTube-Music-Authentication-da) · [Bahasa Melayu](Step-2-YouTube-Music-Authentication-ms)
|
||||
|
||||
## What the step does
|
||||
|
||||
YouTube Music has no simple API key. The `ytmusicapi` library works by borrowing the login of your browser: you copy the request headers of a logged-in Firefox session once (see [Setup and Run](Setup-and-Run)), and `ytmusicapi browser` turns them into `browser.json`. The program then opens `YTMusic("browser.json")` and counts your library playlists.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --check-login
|
||||
```
|
||||
|
||||
On success it prints `Found N playlists in your library.` Without a valid `browser.json` it prints the renewal steps and exits with code 1, instead of a stack trace.
|
||||
|
||||
The screenshot shows the run **without** `browser.json` and the tests that use a fake YouTube Music. A live screenshot is added when `browser.json` exists.
|
||||
|
||||

|
||||
|
||||
## What we gained
|
||||
|
||||
- **How cookie-based authentication works.** Your browser proves who you are with cookies and headers on every request; copying them lets a script act as that session. That is also why `browser.json` is a secret, is ignored by git and expires after a while.
|
||||
- **Hiding a library behind a port.** The program talks to a `MusicService` interface; only `infrastructure/ytmusic.py` imports `ytmusicapi`. The tests use a fake, and a different music service could replace it.
|
||||
- **Failing helpfully.** A missing or expired file becomes a clear message that says how to renew it, because the error a user sees is part of the product.
|
||||
- **A lazy connection.** The client is created on first use, so building the service never touches the network.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Trin 3 - Opret playlisten
|
||||
|
||||
🌐 **English** · [Dansk](Step-3-Create-the-Playlist-da) · [Bahasa Melayu](Step-3-Create-the-Playlist-ms)
|
||||
|
||||
## Hvad trinnet gør
|
||||
|
||||
Programmet opretter en privat playliste med navnet `{date} Billboard 100`, for eksempel `2026-04-18 Billboard 100`, med `yt.create_playlist(title, description, privacy_status="PRIVATE")`. Før det opretter noget, gennemløber det `yt.get_library_playlists()`; hvis der findes en playliste med samme titel, bruger det den.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --create-playlist
|
||||
```
|
||||
|
||||
Skærmbilledet viser testene til dette trin med en falsk YouTube Music-tjeneste: det første kald opretter én playliste, og det andet kald for samme dato opretter ingen. Et rigtigt skærmbillede tilføjes, når `browser.json` findes.
|
||||
|
||||

|
||||
|
||||
## Det lærte vi
|
||||
|
||||
- **Idempotens.** En handling, du kan køre to gange med samme resultat, er sikker at gentage. Ved først at tjekke for en eksisterende playliste fylder et nedbrud eller en ekstra kørsel aldrig biblioteket med dubletter.
|
||||
- **Ét sted for et navn.** Playlistens titel og beskrivelse bygges af skabeloner i `constants.py` af én funktion, så tjekket og oprettelsen aldrig kan være uenige om titlen.
|
||||
- **At læse en API's rigtige svar.** `create_playlist` returnerer playlistens id ved succes, men en fejlordbog ved fejl; adapteren tjekker, hvilket af dem den fik, og rejser en tydelig fejl.
|
||||
- **Privatliv som standard.** Når playlisten oprettes som `PRIVATE`, udgiver en testkørsel aldrig noget.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Langkah 3 - Mencipta Senarai Main
|
||||
|
||||
🌐 **English** · [Dansk](Step-3-Create-the-Playlist-da) · [Bahasa Melayu](Step-3-Create-the-Playlist-ms)
|
||||
|
||||
## Apa yang dilakukan oleh langkah ini
|
||||
|
||||
Program mencipta senarai main peribadi bernama `{date} Billboard 100`, contohnya `2026-04-18 Billboard 100`, dengan `yt.create_playlist(title, description, privacy_status="PRIVATE")`. Sebelum mencipta apa-apa, ia menggelung melalui `yt.get_library_playlists()`; jika senarai main dengan tajuk yang sama wujud, ia menggunakan yang itu.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --create-playlist
|
||||
```
|
||||
|
||||
Tangkapan skrin menunjukkan ujian langkah ini dengan YouTube Music tiruan: panggilan pertama mencipta satu senarai main, panggilan kedua untuk tarikh yang sama tidak mencipta apa-apa. Tangkapan skrin langsung akan ditambah apabila `browser.json` wujud.
|
||||
|
||||

|
||||
|
||||
## Apa yang kita pelajari
|
||||
|
||||
- **Idempotensi.** Operasi yang boleh dijalankan dua kali dengan hasil yang sama selamat untuk diulang. Menyemak senarai main sedia ada terlebih dahulu bermakna ranap atau larian kedua tidak pernah memenuhi pustaka dengan pendua.
|
||||
- **Satu tempat untuk sebuah nama.** Tajuk dan penerangan senarai main dibina daripada templat dalam `constants.py` oleh satu fungsi, jadi semakan dan penciptaan tidak akan pernah berbeza tentang tajuknya.
|
||||
- **Membaca jawapan sebenar sesebuah API.** `create_playlist` mengembalikan id senarai main jika berjaya tetapi kamus ralat jika gagal; penyesuai menyemak yang mana satu diterima dan mencetuskan ralat yang jelas.
|
||||
- **Privasi secara lalai.** Mencipta senarai main sebagai `PRIVATE` bermakna larian ujian tidak pernah menerbitkan apa-apa.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Step 3 - Create the Playlist
|
||||
|
||||
🌐 **English** · [Dansk](Step-3-Create-the-Playlist-da) · [Bahasa Melayu](Step-3-Create-the-Playlist-ms)
|
||||
|
||||
## What the step does
|
||||
|
||||
The program creates a private playlist named `{date} Billboard 100`, for example `2026-04-18 Billboard 100`, with `yt.create_playlist(title, description, privacy_status="PRIVATE")`. Before it creates anything it loops through `yt.get_library_playlists()`; if a playlist with the same title exists, it uses that one.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine --create-playlist
|
||||
```
|
||||
|
||||
The screenshot shows the tests of this step with a fake YouTube Music: the first call creates one playlist, the second call for the same date creates none. A live screenshot is added when `browser.json` exists.
|
||||
|
||||

|
||||
|
||||
## What we gained
|
||||
|
||||
- **Idempotence.** An operation you can run twice with the same result is safe to repeat. Checking for an existing playlist first means a crash or a second run never fills the library with duplicates.
|
||||
- **One place for a name.** The playlist title and description are built from templates in `constants.py` by one function, so the check and the creation can never disagree about the title.
|
||||
- **Reading an API's real answer.** `create_playlist` returns the playlist id on success but an error dictionary on failure; the adapter checks which one it got and raises a clear error.
|
||||
- **Privacy by default.** Creating the playlist as `PRIVATE` means a test run never publishes anything.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Trin 4 - Søg efter og tilføj sange
|
||||
|
||||
🌐 **English** · [Dansk](Step-4-Search-and-Add-Songs-da) · [Bahasa Melayu](Step-4-Search-and-Add-Songs-ms)
|
||||
|
||||
## Hvad trinnet gør
|
||||
|
||||
For hver sang på hitlisten søger programmet på YouTube Music med titlen og kunstneren, med `yt.search(query, filter="songs")`, så der kun kommer sangresultater tilbage, og tilføjer det øverste resultat med `yt.add_playlist_items()`. En sang, der ikke findes, eller som fejler, springes over, og løkken fortsætter. Til sidst fortæller programmet, hvor mange sange der blev tilføjet, og hvilke der blev sprunget over.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine
|
||||
```
|
||||
|
||||
Skærmbilledet viser testene til dette trin med en falsk YouTube Music-tjeneste: alle sange tilføjes, og en sang, der fejler, springes over uden at stoppe kørslen. Et rigtigt skærmbillede tilføjes, når `browser.json` findes.
|
||||
|
||||

|
||||
|
||||
## Det lærte vi
|
||||
|
||||
- **En løkke, der overlever dårlige elementer.** Én sang ud af hundrede har en mærkelig titel eller intet match. Når vi fanger de specifikke sangfejl inde i løkken og noterer, hvad der blev sprunget over, mister vi ikke de andre ni og halvfems på grund af én fejl.
|
||||
- **At vide, hvad man *ikke* skal fange.** Et mislykket login ville få alle de følgende sange til at fejle også, så godkendelsesfejl springes ikke over: de stopper kørslen. At fange alt med et bart `except` ville skjule det.
|
||||
- **At matche en søgning med et resultat.** Når man søger "title artist" med filteret `songs`, får man det bedste sangmatch; det første resultat er en god nok regel til en playliste, og et resultat, der ikke blev fundet, er et normalt udfald, ikke en fejl.
|
||||
- **At teste tid.** Pausen mellem kaldene injiceres, så testene kører øjeblikkeligt og stadig beviser, at den finder sted.
|
||||
- **Tynd præsentation.** Kommandolinjen læser kun input og udskriver oversigten; hele forløbet er use casen `time_travel`, og derfor kan det testes fra ende til anden med falske tjenester.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Langkah 4 - Mencari dan Menambah Lagu
|
||||
|
||||
🌐 **English** · [Dansk](Step-4-Search-and-Add-Songs-da) · [Bahasa Melayu](Step-4-Search-and-Add-Songs-ms)
|
||||
|
||||
## Apa yang dilakukan oleh langkah ini
|
||||
|
||||
Untuk setiap lagu dalam carta, program mencari di YouTube Music dengan tajuk dan artis, menggunakan `yt.search(query, filter="songs")` supaya hanya hasil lagu dikembalikan, dan menambah hasil teratas dengan `yt.add_playlist_items()`. Lagu yang tidak ditemui, atau yang gagal, dilangkau dan gelung diteruskan. Pada akhirnya program memberitahu berapa banyak lagu telah ditambah dan yang mana dilangkau.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine
|
||||
```
|
||||
|
||||
Tangkapan skrin menunjukkan ujian langkah ini dengan YouTube Music tiruan: semua lagu ditambah, dan lagu yang gagal dilangkau tanpa menghentikan larian. Tangkapan skrin langsung akan ditambah apabila `browser.json` wujud.
|
||||
|
||||

|
||||
|
||||
## Apa yang kita pelajari
|
||||
|
||||
- **Gelung yang bertahan menghadapi item yang buruk.** Satu lagu daripada seratus mempunyai tajuk yang ganjil atau tiada padanan. Menangkap ralat lagu yang khusus di dalam gelung, dan merekodkan apa yang dilangkau, menghalang satu kegagalan daripada menghilangkan sembilan puluh sembilan yang lain.
|
||||
- **Mengetahui apa yang *tidak* perlu ditangkap.** Log masuk yang gagal akan menyebabkan setiap lagu berikutnya gagal juga, jadi ralat pengesahan tidak dilangkau: ia menghentikan larian. Menangkap segala-galanya dengan `except` kosong akan menyembunyikan perkara itu.
|
||||
- **Memadankan carian dengan hasil.** Mencari "title artist" dengan penapis `songs` memberikan padanan lagu terbaik; hasil pertama ialah peraturan yang cukup baik untuk senarai main, dan hasil tidak ditemui ialah kesudahan biasa, bukan ralat.
|
||||
- **Menguji masa.** Jeda antara panggilan disuntik, jadi ujian berjalan serta-merta dan masih membuktikan bahawa jeda itu berlaku.
|
||||
- **Persembahan yang nipis.** Baris perintah hanya membaca input dan mencetak ringkasan; keseluruhan aliran ialah kes penggunaan `time_travel`, itulah sebabnya ia boleh diuji dari hujung ke hujung dengan perkhidmatan tiruan.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Step 4 - Search and Add Songs
|
||||
|
||||
🌐 **English** · [Dansk](Step-4-Search-and-Add-Songs-da) · [Bahasa Melayu](Step-4-Search-and-Add-Songs-ms)
|
||||
|
||||
## What the step does
|
||||
|
||||
For every song of the chart the program searches YouTube Music with the title and the artist, using `yt.search(query, filter="songs")` so only song results come back, and adds the top result with `yt.add_playlist_items()`. A song that is not found, or that fails, is skipped and the loop goes on. At the end the program tells how many songs were added and which were skipped.
|
||||
|
||||
```bash
|
||||
python -m musical_time_machine
|
||||
```
|
||||
|
||||
The screenshot shows the tests of this step with a fake YouTube Music: all songs are added, and a failing song is skipped without stopping the run. A live screenshot is added when `browser.json` exists.
|
||||
|
||||

|
||||
|
||||
## What we gained
|
||||
|
||||
- **A loop that survives bad items.** One song in a hundred has an odd title or no match. Catching the specific song errors inside the loop, and recording what was skipped, keeps one failure from losing the other ninety-nine.
|
||||
- **Knowing what *not* to catch.** A failed login would make every following song fail too, so authentication errors are not skipped: they stop the run. Catching everything with a bare `except` would hide that.
|
||||
- **Matching a search to a result.** Searching "title artist" with the `songs` filter gives the best song match; the first result is a good enough rule for a playlist, and a not-found result is a normal outcome, not an error.
|
||||
- **Testing time.** The pause between calls is injected, so the tests run instantly and still prove that it happens.
|
||||
- **Thin presentation.** The command line only reads input and prints the summary; the whole flow is the use case `time_travel`, which is why it can be tested end to end with fakes.
|
||||
@@ -0,0 +1,14 @@
|
||||
**Musical Time Machine**
|
||||
|
||||
- [Home](Home)
|
||||
- [Setup and Run](Setup-and-Run)
|
||||
- [Step 1 - Scrape the Billboard Hot 100](Step-1-Scrape-the-Billboard)
|
||||
- [Step 2 - YouTube Music Authentication](Step-2-YouTube-Music-Authentication)
|
||||
- [Step 3 - Create the Playlist](Step-3-Create-the-Playlist)
|
||||
- [Step 4 - Search and Add Songs](Step-4-Search-and-Add-Songs)
|
||||
|
||||
**Languages**
|
||||
|
||||
- English: [Home](Home)
|
||||
- Dansk: [Hjem](Home-da)
|
||||
- Bahasa Melayu: [Utama](Home-ms)
|
||||
@@ -17,6 +17,7 @@ dev = [
|
||||
"pytest>=9.2",
|
||||
"pytest-cov>=7.1",
|
||||
"ruff>=0.17",
|
||||
"yamllint>=1.38",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
Reference in New Issue
Block a user