mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 22:41:59 +03:00
report: pick individual --extra column groups instead of all-or-nothing
--extra now takes group names (timestamp/duration/version/album/artist/all) so you can add just what you need, e.g. --extra timestamp. Also split README command lists into core vs. extra sections for readability.
This commit is contained in:
parent
5d4cab5d68
commit
b991236ffa
7 changed files with 162 additions and 53 deletions
|
|
@ -181,8 +181,9 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
- `out/report.csv` — a preview of the playlist breakdown before `apply` (including the
|
- `out/report.csv` — a preview of the playlist breakdown before `apply` (including the
|
||||||
`fine_bucket` column — the sub-genre before any collapsing into a top-level bucket).
|
`fine_bucket` column — the sub-genre before any collapsing into a top-level bucket).
|
||||||
`report --order playlist` keeps source track order instead of sorting by target playlist;
|
`report --order playlist` keeps source track order instead of sorting by target playlist;
|
||||||
`report --extra` adds the columns listed above (`added_at`, `duration_ms`, `release_date`,
|
`report --extra <group...>` adds the selected groups' columns (`timestamp`/`duration`/
|
||||||
`artist_rating_month/week/day`, etc. - full list in `report.EXTRA_FIELDNAMES`).
|
`version`/`album`/`artist`, or `all` for every group; `report.EXTRA_GROUPS` -> column list,
|
||||||
|
`report.resolve_extra_columns()` expands group names into a column list for the CLI).
|
||||||
- `out/digest.md` — a compact library summary for pasting into an LLM chat (top artists, top
|
- `out/digest.md` — a compact library summary for pasting into an LLM chat (top artists, top
|
||||||
albums, genre/language/decade breakdowns).
|
albums, genre/language/decade breakdowns).
|
||||||
- `out/report_source.csv`, `out/digest_source.md` — the same reports for a `--source` run; a
|
- `out/report_source.csv`, `out/digest_source.md` — the same reports for a `--source` run; a
|
||||||
|
|
|
||||||
|
|
@ -179,9 +179,10 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
- `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null).
|
- `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null).
|
||||||
- `out/report.csv` — предпросмотр распределения по плейлистам перед `apply` (включая колонку
|
- `out/report.csv` — предпросмотр распределения по плейлистам перед `apply` (включая колонку
|
||||||
`fine_bucket` — под-жанр до возможного схлопывания в крупную корзину). `report --order playlist`
|
`fine_bucket` — под-жанр до возможного схлопывания в крупную корзину). `report --order playlist`
|
||||||
сохраняет исходный порядок треков вместо сортировки по целевому плейлисту; `report --extra`
|
сохраняет исходный порядок треков вместо сортировки по целевому плейлисту; `report --extra
|
||||||
добавляет колонки из списка выше (`added_at`, `duration_ms`, `release_date`,
|
<группа...>` добавляет колонки выбранных групп (`timestamp`/`duration`/`version`/`album`/`artist`,
|
||||||
`artist_rating_month/week/day` и т.д. - полный список в `report.EXTRA_FIELDNAMES`).
|
или `all` - сразу все; `report.EXTRA_GROUPS` -> список колонок, `report.resolve_extra_columns()`
|
||||||
|
разворачивает имена групп в список колонок для CLI).
|
||||||
- `out/digest.md` — сжатая сводка библиотеки для вставки в чат с LLM (топ-исполнители, топ-альбомы,
|
- `out/digest.md` — сжатая сводка библиотеки для вставки в чат с LLM (топ-исполнители, топ-альбомы,
|
||||||
распределение по жанрам, языку и десятилетиям).
|
распределение по жанрам, языку и десятилетиям).
|
||||||
- `out/report_source.csv`, `out/digest_source.md` — те же отчёты, но для запуска с `--source`;
|
- `out/report_source.csv`, `out/digest_source.md` — те же отчёты, но для запуска с `--source`;
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -33,21 +33,44 @@ For development (tests):
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
|
### Core commands
|
||||||
|
|
||||||
```
|
```
|
||||||
.venv\Scripts\python -m sort_ym auth # log in to a Yandex account via device-flow and save the token to .token
|
.venv\Scripts\python -m sort_ym auth # log in to a Yandex account via device-flow and save the token to .token
|
||||||
.venv\Scripts\python -m sort_ym fetch # download liked tracks (cache/tracks.json) and their artists' genres (cache/artist_genres.json); does not change the account
|
.venv\Scripts\python -m sort_ym fetch # download liked tracks (cache/tracks.json) and their artists' genres (cache/artist_genres.json); does not change the account
|
||||||
.venv\Scripts\python -m sort_ym report # compute genre and language per track, save out/report.csv (does not change the account)
|
.venv\Scripts\python -m sort_ym report # compute genre and language per track, save out/report.csv (does not change the account)
|
||||||
.venv\Scripts\python -m sort_ym report --order playlist # skip playlist-based sorting - keep source track order (as in likes/the --source playlist)
|
|
||||||
.venv\Scripts\python -m sort_ym report --extra # + added_at/duration_ms/release_date/artist rating columns etc.
|
|
||||||
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # dry run: create playlists and add the first 20 tracks
|
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # dry run: create playlists and add the first 20 tracks
|
||||||
.venv\Scripts\python -m sort_ym apply --yes # create playlists and add all tracks (changes the account)
|
.venv\Scripts\python -m sort_ym apply --yes # create playlists and add all tracks (changes the account)
|
||||||
.venv\Scripts\python -m sort_ym digest # library summary (top artists, genres) to out/digest.md - to paste into an LLM chat; no network used
|
.venv\Scripts\python -m sort_ym digest # library summary (top artists, genres) to out/digest.md - to paste into an LLM chat; no network used
|
||||||
|
```
|
||||||
|
|
||||||
|
On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added.
|
||||||
|
|
||||||
|
### Extra: another playlist instead of your likes
|
||||||
|
|
||||||
|
```
|
||||||
.venv\Scripts\python -m sort_ym report --source <url> # same, but for someone else's/another playlist by link
|
.venv\Scripts\python -m sort_ym report --source <url> # same, but for someone else's/another playlist by link
|
||||||
.venv\Scripts\python -m sort_ym digest --source <url> # digest of that playlist
|
.venv\Scripts\python -m sort_ym digest --source <url> # digest of that playlist
|
||||||
.venv\Scripts\python -m sort_ym apply --source <url> --label Friend --yes # sort its tracks into your own "Genre — RU (Friend)" playlists
|
.venv\Scripts\python -m sort_ym apply --source <url> --label Friend --yes # sort its tracks into your own "Genre — RU (Friend)" playlists
|
||||||
```
|
```
|
||||||
|
|
||||||
On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added.
|
See "Analyzing and sorting another playlist" below for details.
|
||||||
|
|
||||||
|
### Extra: fine-tuning report.csv
|
||||||
|
|
||||||
|
By default `report` groups/sorts tracks by target playlist with a base set of columns.
|
||||||
|
These flags change that — from most to least commonly useful:
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv\Scripts\python -m sort_ym report --order playlist # skip playlist-based sorting - keep source track order (as in likes/the --source playlist)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra timestamp # + when the track was added (added_at)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra album # + album release date and likes (release_date, album_likes_count)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra artist # + first artist's track counts/rating
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra duration # + track length (duration_ms)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra version # + track/album version tags (remix, anniversary edition, etc.)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra timestamp album # groups can be combined, space-separated
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra all --order playlist # every extra column + source track order
|
||||||
|
```
|
||||||
|
|
||||||
## Full cache cleanup
|
## Full cache cleanup
|
||||||
|
|
||||||
|
|
|
||||||
29
README.ru.md
29
README.ru.md
|
|
@ -33,21 +33,44 @@ python -m venv .venv
|
||||||
|
|
||||||
## Что делает
|
## Что делает
|
||||||
|
|
||||||
|
### Основные команды
|
||||||
|
|
||||||
```
|
```
|
||||||
.venv\Scripts\python -m sort_ym auth # войти в аккаунт Яндекса через device-flow и сохранить токен в .token
|
.venv\Scripts\python -m sort_ym auth # войти в аккаунт Яндекса через device-flow и сохранить токен в .token
|
||||||
.venv\Scripts\python -m sort_ym fetch # скачать лайкнутые треки (cache/tracks.json) и жанры их артистов (cache/artist_genres.json); аккаунт не меняет
|
.venv\Scripts\python -m sort_ym fetch # скачать лайкнутые треки (cache/tracks.json) и жанры их артистов (cache/artist_genres.json); аккаунт не меняет
|
||||||
.venv\Scripts\python -m sort_ym report # посчитать жанр и язык каждого трека, сохранить out/report.csv (аккаунт не меняет)
|
.venv\Scripts\python -m sort_ym report # посчитать жанр и язык каждого трека, сохранить out/report.csv (аккаунт не меняет)
|
||||||
.venv\Scripts\python -m sort_ym report --order playlist # без сортировки по плейлистам - треки в исходном порядке (как в лайках/--source плейлисте)
|
|
||||||
.venv\Scripts\python -m sort_ym report --extra # + колонки added_at/duration_ms/release_date/рейтинг артиста и т.п.
|
|
||||||
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков
|
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков
|
||||||
.venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт)
|
.venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт)
|
||||||
.venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется
|
.venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется
|
||||||
|
```
|
||||||
|
|
||||||
|
При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки.
|
||||||
|
|
||||||
|
### Дополнительно: другой плейлист вместо лайков
|
||||||
|
|
||||||
|
```
|
||||||
.venv\Scripts\python -m sort_ym report --source <ссылка> # то же самое, но для чужого/другого плейлиста по ссылке
|
.venv\Scripts\python -m sort_ym report --source <ссылка> # то же самое, но для чужого/другого плейлиста по ссылке
|
||||||
.venv\Scripts\python -m sort_ym digest --source <ссылка> # дайджест чужого/другого плейлиста
|
.venv\Scripts\python -m sort_ym digest --source <ссылка> # дайджест чужого/другого плейлиста
|
||||||
.venv\Scripts\python -m sort_ym apply --source <ссылка> --label Друг --yes # раскидать его треки по своим плейлистам "Жанр — RU (Друг)"
|
.venv\Scripts\python -m sort_ym apply --source <ссылка> --label Друг --yes # раскидать его треки по своим плейлистам "Жанр — RU (Друг)"
|
||||||
```
|
```
|
||||||
|
|
||||||
При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки.
|
Подробнее — раздел «Анализ и раскидка другого плейлиста» ниже.
|
||||||
|
|
||||||
|
### Дополнительно: гибкая настройка report.csv
|
||||||
|
|
||||||
|
По умолчанию `report` группирует и сортирует треки по целевому плейлисту с базовым набором колонок.
|
||||||
|
Эти флаги это меняют — от самого востребованного к более редкому:
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv\Scripts\python -m sort_ym report --order playlist # без сортировки по плейлистам - треки в исходном порядке источника (как в лайках/--source плейлисте)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra timestamp # + дата добавления трека (added_at)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra album # + дата релиза и лайки альбома (release_date, album_likes_count)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra artist # + счётчики/рейтинг первого артиста трека
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra duration # + длительность трека (duration_ms)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra version # + пометки версии трека/альбома (ремикс, юбилейное издание и т.п.)
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra timestamp album # можно комбинировать несколько групп через пробел
|
||||||
|
.venv\Scripts\python -m sort_ym report --extra all --order playlist # все доп. колонки сразу + порядок как в источнике
|
||||||
|
```
|
||||||
|
|
||||||
## Полная очистка кэша
|
## Полная очистка кэша
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,15 @@ def _print_report_summary(rows: list[dict], out_file) -> None:
|
||||||
|
|
||||||
def cmd_report(args: argparse.Namespace) -> None:
|
def cmd_report(args: argparse.Namespace) -> None:
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
extra_columns = report.resolve_extra_columns(args.extra) if args.extra else None
|
||||||
|
|
||||||
if args.source:
|
if args.source:
|
||||||
ctx = _source_context(cfg, args.source)
|
ctx = _source_context(cfg, args.source)
|
||||||
rows = report.build_rows(
|
rows = report.build_rows(
|
||||||
ctx.tracks, ctx.lang_cache, ctx.catalog, ctx.artist_genres, cfg.small_group_min,
|
ctx.tracks, ctx.lang_cache, ctx.catalog, ctx.artist_genres, cfg.small_group_min,
|
||||||
order=args.order, extra_fields=args.extra,
|
order=args.order, extra_columns=extra_columns,
|
||||||
)
|
)
|
||||||
out_file = report.write_report(rows, cfg.out_dir, report.REPORT_SOURCE_FILE, extra_fields=args.extra)
|
out_file = report.write_report(rows, cfg.out_dir, report.REPORT_SOURCE_FILE, extra_columns=extra_columns)
|
||||||
_print_report_summary(rows, out_file)
|
_print_report_summary(rows, out_file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -104,9 +105,9 @@ def cmd_report(args: argparse.Namespace) -> None:
|
||||||
|
|
||||||
rows = report.build_rows(
|
rows = report.build_rows(
|
||||||
tracks_cache, lang_cache, catalog, artist_genres, cfg.small_group_min,
|
tracks_cache, lang_cache, catalog, artist_genres, cfg.small_group_min,
|
||||||
order=args.order, extra_fields=args.extra,
|
order=args.order, extra_columns=extra_columns,
|
||||||
)
|
)
|
||||||
out_file = report.write_report(rows, cfg.out_dir, extra_fields=args.extra)
|
out_file = report.write_report(rows, cfg.out_dir, extra_columns=extra_columns)
|
||||||
_print_report_summary(rows, out_file)
|
_print_report_summary(rows, out_file)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -207,9 +208,10 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
"playlist - как треки идут в источнике (в лайках или в --source плейлисте), без сортировки",
|
"playlist - как треки идут в источнике (в лайках или в --source плейлисте), без сортировки",
|
||||||
)
|
)
|
||||||
p_report.add_argument(
|
p_report.add_argument(
|
||||||
"--extra", action="store_true",
|
"--extra", nargs="+", metavar="GROUP", choices=[*report.EXTRA_GROUPS, "all"], default=None,
|
||||||
help="добавить в CSV колонки с доп. данными API: дата добавления, длительность, дата релиза "
|
help="добавить в CSV колонки указанных групп (можно несколько через пробел), или 'all' - сразу все: "
|
||||||
"альбома, рейтинг/счётчики первого артиста трека",
|
"timestamp (added_at), duration (duration_ms), version (track_version/album_version), "
|
||||||
|
"album (release_date/album_likes_count), artist (счётчики и рейтинг первого артиста трека)",
|
||||||
)
|
)
|
||||||
|
|
||||||
p_apply = sub.add_parser("apply", help="создать плейлисты и добавить треки")
|
p_apply = sub.add_parser("apply", help="создать плейлисты и добавить треки")
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,15 @@ from . import classify, genres, language
|
||||||
|
|
||||||
FIELDNAMES = ["title", "artists", "genre_raw", "fine_bucket", "bucket", "lang", "target_playlist", "id", "album_id"]
|
FIELDNAMES = ["title", "artists", "genre_raw", "fine_bucket", "bucket", "lang", "target_playlist", "id", "album_id"]
|
||||||
|
|
||||||
# Только вместе с --extra: сырые поля API, которых нет в базовом отчёте (см. обсуждение в чате -
|
# Сырые поля API, которых нет в базовом отчёте - подключаются группами через --extra
|
||||||
# added_at из TrackShort.timestamp, остальное - из Track/Album/Artist, но не заполнено стабильно
|
# (см. обсуждение в чате: added_at из TrackShort.timestamp, остальное - из Track/Album/Artist).
|
||||||
# у part данных вроде explicit/lyrics_info, поэтому в набор не включено).
|
# Порядок в EXTRA_FIELDNAMES - канонический порядок колонок в CSV независимо от порядка групп в --extra.
|
||||||
EXTRA_FIELDNAMES = [
|
EXTRA_GROUPS: dict[str, list[str]] = {
|
||||||
"added_at",
|
"timestamp": ["added_at"],
|
||||||
"duration_ms",
|
"duration": ["duration_ms"],
|
||||||
"track_version",
|
"version": ["track_version", "album_version"],
|
||||||
"album_version",
|
"album": ["release_date", "album_likes_count"],
|
||||||
"release_date",
|
"artist": [
|
||||||
"album_likes_count",
|
|
||||||
"artist_track_count",
|
"artist_track_count",
|
||||||
"artist_direct_albums",
|
"artist_direct_albums",
|
||||||
"artist_also_albums",
|
"artist_also_albums",
|
||||||
|
|
@ -24,20 +23,36 @@ EXTRA_FIELDNAMES = [
|
||||||
"artist_rating_month",
|
"artist_rating_month",
|
||||||
"artist_rating_week",
|
"artist_rating_week",
|
||||||
"artist_rating_day",
|
"artist_rating_day",
|
||||||
]
|
],
|
||||||
|
}
|
||||||
|
EXTRA_FIELDNAMES = [col for cols in EXTRA_GROUPS.values() for col in cols]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_columns(group_names: list[str]) -> list[str]:
|
||||||
|
"""--extra timestamp album -> ["added_at", "release_date", "album_likes_count"] (канонический порядок).
|
||||||
|
|
||||||
|
"all" - алиас на все группы сразу.
|
||||||
|
"""
|
||||||
|
names = list(EXTRA_GROUPS) if "all" in group_names else group_names
|
||||||
|
unknown = set(names) - set(EXTRA_GROUPS)
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(f"неизвестные группы --extra: {sorted(unknown)}, доступны: {list(EXTRA_GROUPS)} или 'all'")
|
||||||
|
selected = {col for name in names for col in EXTRA_GROUPS[name]}
|
||||||
|
return [col for col in EXTRA_FIELDNAMES if col in selected]
|
||||||
|
|
||||||
|
|
||||||
REPORT_FILE = "report.csv"
|
REPORT_FILE = "report.csv"
|
||||||
REPORT_SOURCE_FILE = "report_source.csv"
|
REPORT_SOURCE_FILE = "report_source.csv"
|
||||||
|
|
||||||
|
|
||||||
def _extra_row_fields(t: dict, artist_genres: dict[str, dict]) -> dict:
|
def _extra_row_fields(t: dict, artist_genres: dict[str, dict], columns: list[str]) -> dict:
|
||||||
# Счётчики/рейтинг берём у первого артиста трека - у треков с несколькими артистами
|
# Счётчики/рейтинг берём у первого артиста трека - у треков с несколькими артистами
|
||||||
# остальные не имеют выделенной колонки (см. обсуждение в чате).
|
# остальные не имеют выделенной колонки (см. обсуждение в чате).
|
||||||
artist_ids = t.get("artist_ids", [])
|
artist_ids = t.get("artist_ids", [])
|
||||||
artist = artist_genres.get(str(artist_ids[0])) if artist_ids else None
|
artist = artist_genres.get(str(artist_ids[0])) if artist_ids else None
|
||||||
counts = (artist or {}).get("counts") or {}
|
counts = (artist or {}).get("counts") or {}
|
||||||
ratings = (artist or {}).get("ratings") or {}
|
ratings = (artist or {}).get("ratings") or {}
|
||||||
return {
|
all_values = {
|
||||||
"added_at": t.get("added_at"),
|
"added_at": t.get("added_at"),
|
||||||
"duration_ms": t.get("duration_ms"),
|
"duration_ms": t.get("duration_ms"),
|
||||||
"track_version": t.get("track_version") or "",
|
"track_version": t.get("track_version") or "",
|
||||||
|
|
@ -52,6 +67,7 @@ def _extra_row_fields(t: dict, artist_genres: dict[str, dict]) -> dict:
|
||||||
"artist_rating_week": ratings.get("week"),
|
"artist_rating_week": ratings.get("week"),
|
||||||
"artist_rating_day": ratings.get("day"),
|
"artist_rating_day": ratings.get("day"),
|
||||||
}
|
}
|
||||||
|
return {col: all_values[col] for col in columns}
|
||||||
|
|
||||||
|
|
||||||
def build_rows(
|
def build_rows(
|
||||||
|
|
@ -61,7 +77,7 @@ def build_rows(
|
||||||
artist_genres: dict[str, dict],
|
artist_genres: dict[str, dict],
|
||||||
small_group_min: int,
|
small_group_min: int,
|
||||||
order: str = "grouped",
|
order: str = "grouped",
|
||||||
extra_fields: bool = False,
|
extra_columns: list[str] | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
# Проход 1: под-жанр (fine) и язык на каждый трек, размеры групп (fine, lang) фиксируем сразу.
|
# Проход 1: под-жанр (fine) и язык на каждый трек, размеры групп (fine, lang) фиксируем сразу.
|
||||||
prelim = []
|
prelim = []
|
||||||
|
|
@ -101,8 +117,8 @@ def build_rows(
|
||||||
"id": t["id"],
|
"id": t["id"],
|
||||||
"album_id": t["album_id"],
|
"album_id": t["album_id"],
|
||||||
}
|
}
|
||||||
if extra_fields:
|
if extra_columns:
|
||||||
row.update(_extra_row_fields(t, artist_genres))
|
row.update(_extra_row_fields(t, artist_genres, extra_columns))
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
# "playlist" - порядок как в источнике (tracks_cache сохраняет порядок плейлиста, см. source.py);
|
# "playlist" - порядок как в источнике (tracks_cache сохраняет порядок плейлиста, см. source.py);
|
||||||
|
|
@ -114,10 +130,10 @@ def build_rows(
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def write_report(rows: list[dict], out_dir: Path, filename: str = REPORT_FILE, extra_fields: bool = False) -> Path:
|
def write_report(rows: list[dict], out_dir: Path, filename: str = REPORT_FILE, extra_columns: list[str] | None = None) -> Path:
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
out_file = out_dir / filename
|
out_file = out_dir / filename
|
||||||
fieldnames = FIELDNAMES + EXTRA_FIELDNAMES if extra_fields else FIELDNAMES
|
fieldnames = FIELDNAMES + extra_columns if extra_columns else FIELDNAMES
|
||||||
with out_file.open("w", encoding="utf-8-sig", newline="") as f:
|
with out_file.open("w", encoding="utf-8-sig", newline="") as f:
|
||||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sort_ym import report
|
from sort_ym import report
|
||||||
|
|
||||||
CATALOG: dict = {} # classify_track с пустыми artist_genre_lists использует только genre_raw
|
CATALOG: dict = {} # classify_track с пустыми artist_genre_lists использует только genre_raw
|
||||||
|
|
@ -105,7 +107,20 @@ def test_order_grouped_sorts_by_target_playlist():
|
||||||
assert rows == sorted(rows, key=lambda r: (r["target_playlist"], r["artists"], r["title"]))
|
assert rows == sorted(rows, key=lambda r: (r["target_playlist"], r["artists"], r["title"]))
|
||||||
|
|
||||||
|
|
||||||
def test_extra_fields_adds_columns_from_track_and_artist_cache():
|
def test_resolve_extra_columns_expands_single_group():
|
||||||
|
assert report.resolve_extra_columns(["timestamp"]) == ["added_at"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_extra_columns_all_expands_every_group_in_canonical_order():
|
||||||
|
assert report.resolve_extra_columns(["all"]) == report.EXTRA_FIELDNAMES
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_extra_columns_rejects_unknown_group():
|
||||||
|
with pytest.raises(ValueError, match="bogus"):
|
||||||
|
report.resolve_extra_columns(["bogus"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_columns_only_adds_requested_group():
|
||||||
tracks_cache = {
|
tracks_cache = {
|
||||||
"1:1": {
|
"1:1": {
|
||||||
**_track(1, "T", ["A"], "punk", artist_ids=[42]),
|
**_track(1, "T", ["A"], "punk", artist_ids=[42]),
|
||||||
|
|
@ -125,7 +140,35 @@ def test_extra_fields_adds_columns_from_track_and_artist_cache():
|
||||||
genre_catalog=CATALOG,
|
genre_catalog=CATALOG,
|
||||||
artist_genres=artist_genres,
|
artist_genres=artist_genres,
|
||||||
small_group_min=12,
|
small_group_min=12,
|
||||||
extra_fields=True,
|
extra_columns=report.resolve_extra_columns(["timestamp"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
row = rows[0]
|
||||||
|
assert row["added_at"] == "2026-01-01T00:00:00+00:00"
|
||||||
|
assert "duration_ms" not in row, "запрошена только группа timestamp - duration не должно быть в строке"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_columns_all_adds_every_group():
|
||||||
|
tracks_cache = {
|
||||||
|
"1:1": {
|
||||||
|
**_track(1, "T", ["A"], "punk", artist_ids=[42]),
|
||||||
|
"added_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
"duration_ms": 123000,
|
||||||
|
"track_version": "Remix",
|
||||||
|
"album_version": None,
|
||||||
|
"release_date": "2020-05-01T00:00:00+03:00",
|
||||||
|
"album_likes_count": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
artist_genres = {"42": {"name": "A", "genres": [], "counts": {"tracks": 5}, "ratings": {"month": 7}}}
|
||||||
|
|
||||||
|
rows = report.build_rows(
|
||||||
|
tracks_cache=tracks_cache,
|
||||||
|
lang_cache={},
|
||||||
|
genre_catalog=CATALOG,
|
||||||
|
artist_genres=artist_genres,
|
||||||
|
small_group_min=12,
|
||||||
|
extra_columns=report.resolve_extra_columns(["all"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
row = rows[0]
|
row = rows[0]
|
||||||
|
|
@ -137,14 +180,14 @@ def test_extra_fields_adds_columns_from_track_and_artist_cache():
|
||||||
assert row["artist_rating_month"] == 7
|
assert row["artist_rating_month"] == 7
|
||||||
|
|
||||||
|
|
||||||
def test_write_report_extra_fields_writes_extended_header(tmp_path):
|
def test_write_report_extra_columns_writes_only_selected_header(tmp_path):
|
||||||
rows = [{"title": "T", "artists": "A", "genre_raw": "punk", "fine_bucket": "punk", "bucket": "punk", "lang": "RU", "target_playlist": "Панк — RU", "id": 1, "album_id": 10, "added_at": "x", "duration_ms": 1, "track_version": "", "album_version": "", "release_date": "y", "album_likes_count": 1, "artist_track_count": 1, "artist_direct_albums": 1, "artist_also_albums": 1, "artist_also_tracks": 1, "artist_rating_month": 1, "artist_rating_week": 1, "artist_rating_day": 1}]
|
rows = [{"title": "T", "artists": "A", "genre_raw": "punk", "fine_bucket": "punk", "bucket": "punk", "lang": "RU", "target_playlist": "Панк — RU", "id": 1, "album_id": 10, "added_at": "x"}]
|
||||||
|
|
||||||
out_file = report.write_report(rows, tmp_path, extra_fields=True)
|
out_file = report.write_report(rows, tmp_path, extra_columns=report.resolve_extra_columns(["timestamp"]))
|
||||||
|
|
||||||
header = out_file.read_text(encoding="utf-8-sig").splitlines()[0]
|
header = out_file.read_text(encoding="utf-8-sig").splitlines()[0]
|
||||||
assert "added_at" in header
|
assert "added_at" in header
|
||||||
assert "artist_rating_day" in header
|
assert "artist_rating_day" not in header
|
||||||
|
|
||||||
|
|
||||||
def test_collapse_is_single_pass_and_terminates():
|
def test_collapse_is_single_pass_and_terminates():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue