diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c2f00..0a23715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,16 @@ # Changelog -Changes to drift. Loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +All notable changes to Drift are documented in this file. -## Unreleased +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added -- MPRIS on linux. media keys, `playerctl` and waybar can now see and control drift (play/pause/next/prev/seek/stop), with title, artist, album and cover art. Shows up as `org.mpris.MediaPlayer2.drift`. If theres no dbus session it just doesnt start, nothing breaks. -- `:album ` to search albums, `:playlist ` to search youtube music playlists. Results show up in a picker, enter loads the tracks. (`:playlists` is still your own playlists tab.) Big playlists stop at ~300 tracks. -- `gb` / `:album` / `G` then `b` opens the album of the selected song. It finds it by album + artist name so it can guess wrong sometimes, and the song needs album info to begin with. -- `gs` / `:similar` / `:radio` loads ~50 songs similar to the selected one and starts playing them, like youtube music radio. -- Synced lyrics from LRCLIB. `L` toggles the side panel, `F` for fullscreen. Current line gets a progress fill, lines around it fade. -- Plain lyrics fallback when theres no synced version, scroll with `j`/`k`. +- Synced lyrics via LRCLIB. `L` or `:lyrics` toggles a side panel, `F` opens fullscreen. +- Current lyric line is highlighted with a progress fill; nearby lines fade out. +- Plain lyrics fallback (scroll with `j`/`k`) when no synced version exists. ### Changed -- Album tracks now get the artist/album/length filled in when youtube leaves them out. -- Tracks from a playlist get their album too, so `gb` works on them. -- New dependency on linux: `souvlaki` (zbus backend, pure rust, no system libs). `Cargo.lock` needs a regen, just run `cargo build` once. `--locked` builds, the flake and the AUR package need the new lock. -- Tab bar is pills now, inactive tabs have dimmed counts. -- Tab bar only reserves space for the spinner while busy, fits narrow terminals better. - -### Fixed - -- Playing from a playlist no longer replaces the queue. Playlists play as their own list, queue only has what you add, queued songs go first and then the playlist continues. -- Shuffle and repeat-all work for playlists and results now. repeat-all used to loop the queue instead of the list you were actualy playing. -- `P` / `:playall` doesnt wipe the queue anymore. +- Tab bar redesigned as pills with dimmed counts on inactive tabs. +- Tab bar only reserves spinner space while busy, so tabs fit on narrow terminals. diff --git a/Cargo.lock b/Cargo.lock index 412aff4..2eaed14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,15 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -29,239 +20,18 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "async-broadcast" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" -dependencies = [ - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.5.0", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "blocking", - "futures-lite 1.13.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.5", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.2", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.44", - "windows-sys 0.48.0", -] - -[[package]] -name = "async-recursion" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io 2.6.0", - "async-lock 3.4.2", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.5", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "cassowary" version = "0.3.0" @@ -279,9 +49,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.9.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "shlex", @@ -293,36 +63,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "compact_str" version = "0.8.2" @@ -337,64 +77,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.2" @@ -404,23 +86,17 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.13.2", + "bitflags", "crossterm_winapi", "mio", "parking_lot", - "rustix 0.38.44", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -435,16 +111,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.24.1" @@ -465,7 +131,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -476,28 +142,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.9", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", + "syn 3.0.6", ] [[package]] @@ -521,12 +166,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "displaydoc" version = "0.2.7" @@ -535,7 +174,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -548,7 +187,6 @@ dependencies = [ "ratatui", "serde", "serde_json", - "souvlaki", "toml", "ureq", ] @@ -559,32 +197,11 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "equivalent" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -596,63 +213,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.2", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - [[package]] name = "find-msvc-tools" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "flate2" @@ -671,21 +236,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -695,83 +245,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.5.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -783,17 +256,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -817,24 +279,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "icu_collections" version = "2.3.0" @@ -957,9 +401,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.8" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] @@ -974,27 +418,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 3.0.9", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", + "syn 3.0.6", ] [[package]] @@ -1014,9 +438,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "libc" -version = "0.2.191" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libredox" @@ -1027,24 +451,12 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.3" @@ -1075,39 +487,12 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miniz_oxide" version = "0.9.1" @@ -1130,27 +515,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -1163,22 +527,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1214,59 +562,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand 2.5.0", - "futures-io", -] - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.3", - "pin-project-lite", - "rustix 1.1.5", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - [[package]] name = "potential_utf" version = "0.1.6" @@ -1276,25 +571,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1313,49 +589,13 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "ratatui" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.13.2", + "bitflags", "cassowary", "compact_str", "crossterm", @@ -1376,7 +616,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.2", + "bitflags", ] [[package]] @@ -1385,40 +625,11 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.17", + "getrandom", "libredox", "thiserror", ] -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - [[package]] name = "ring" version = "0.17.14" @@ -1427,57 +638,30 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.2", + "bitflags", "errno", "libc", - "linux-raw-sys 0.4.15", + "linux-raw-sys", "windows-sys 0.59.0", ] -[[package]] -name = "rustix" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" -dependencies = [ - "bitflags 2.13.2", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" -version = "0.23.46" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -1553,14 +737,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.154" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1569,17 +753,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -1589,17 +762,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "2.0.1" @@ -1643,46 +805,11 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "souvlaki" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" -dependencies = [ - "base64", - "block", - "cocoa", - "core-graphics", - "dispatch", - "objc", - "pollster", - "thiserror", - "windows", - "zbus", - "zvariant", -] +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "stable_deref_trait" @@ -1730,17 +857,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.119" @@ -1754,9 +870,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.9" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -1771,20 +887,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand 2.5.0", - "getrandom 0.4.3", - "once_cell", - "rustix 1.1.5", - "windows-sys 0.61.2", + "syn 3.0.6", ] [[package]] @@ -1826,7 +929,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.27", + "toml_edit", ] [[package]] @@ -1838,17 +941,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -1860,7 +952,7 @@ dependencies = [ "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.15", + "winnow", ] [[package]] @@ -1869,59 +961,11 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset 0.9.1", - "tempfile", - "windows-sys 0.61.2", -] - [[package]] name = "unicode-ident" -version = "1.0.27" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -1994,18 +1038,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2018,14 +1050,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.10", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.10" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -2052,15 +1084,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -2103,21 +1126,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -2149,12 +1157,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2167,12 +1169,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2185,12 +1181,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2209,12 +1199,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2227,12 +1211,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2245,12 +1223,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2263,12 +1235,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2281,15 +1247,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.15" @@ -2305,16 +1262,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "yoke" version = "0.8.3" @@ -2334,96 +1281,10 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", "synstructure", ] -[[package]] -name = "zbus" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "byteorder", - "derivative", - "enumflags2", - "event-listener 2.5.3", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "once_cell", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "winapi", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "zerofrom" version = "0.1.8" @@ -2441,7 +1302,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", "synstructure", ] @@ -2481,7 +1342,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -2495,41 +1356,3 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zvariant" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] diff --git a/Cargo.toml b/Cargo.toml index 971a155..582b230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,6 @@ ureq = { version = "2", features = ["json"] } dirs = "5" libc = "0.2" -[target.'cfg(target_os = "linux")'.dependencies] -souvlaki = { version = "0.8", default-features = false, features = ["use_zbus"] } - [profile.release] lto = true codegen-units = 1 diff --git a/README.md b/README.md index d958b1d..d847798 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ Search, queue and play music with no account, no login and no browser. - **Song menu** - press `G` on a song to play, queue, add to a playlist, jump to the artist or see its info - **Command palette** - `:search`, `:seek`, `:vol`, `:import` and more, with completion and history - **Artist browsing** - press `ga` to see an artist's songs -- **Similar songs** - press `gs` to see similar songs - **Audio visualizer** - a live visualizer while music plays +- **Session restore** - your queue and playback position come back on the next launch - **Configurable** - volume, seek step, repeat/shuffle defaults and accent color in a single TOML file ## Installation @@ -116,7 +116,6 @@ Press `/` to search, `Enter` to play, `a` to queue and `?` for the full list of | `J` `K` | move a queue or playlist item down / up | | `G` | song menu | | `ga` | go to the artist | -| `gs` | see similar songs | | `L` | toggle the lyrics panel | | `F` | fullscreen lyrics | | `/` `:` | search / command line | @@ -136,10 +135,6 @@ Press `/` to search, `Enter` to play, `a` to queue and `?` for the full list of | `:save name` `:load name` `:append name` | work with playlists | | `:addto name` `:delpl name` | add a song to / delete a playlist | | `:import URL` | import a YouTube / YouTube Music playlist | -| `:album [query]` | open the selected song's album, or search albums | -| `:playlist query` | search YouTube Music playlists | -| `:similar` | radio: songs similar to the selected one (also `:radio`) | -| `:artist` | songs by the selected song's artist | | `:lyrics` | toggle the lyrics panel | ## Lyrics diff --git a/flake.lock b/flake.lock index bc0b988..b713556 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1792488032, - "narHash": "sha256-RkAXQ3+a5rHMiP97Er7AawR1kvEXX9tK1mhiWedOPTc=", + "lastModified": 1792116957, + "narHash": "sha256-EjZqHKObBfIOeHZnMRVV87HB3pPq/WBkmRJWyFxmZu8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e513db7b0a65def6f6e17bb55195dfe510871944", + "rev": "f20cd90e28fe0403873f473b29e42662cd86e8c4", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 505bb0e..2e60f5c 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ in { default = pkgs.rustPlatform.buildRustPackage { pname = "drift"; - version = "1.3"; + version = "0.1.0"; src = ./.; cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [ pkgs.makeWrapper ]; diff --git a/src/app.rs b/src/app.rs index 2dae383..0d4f26d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,12 +9,11 @@ use ratatui::widgets::TableState; use crate::command::{self, Amount, Command, COMMAND_NAMES}; use crate::config::{self, Config}; use crate::lyrics::{self, Lyrics}; -use crate::model::{split_artists, Collection, RepeatMode, Track}; -use crate::mpris::{Cmd, Mpris}; +use crate::model::{split_artists, RepeatMode, Track}; use crate::player::{Player, PlayerEvent}; use crate::session::Session; use crate::store::Store; -use crate::ytmusic::{self, Kind, Page}; +use crate::ytmusic::{self, Page}; pub enum AppEvent { Player(u64, PlayerEvent), @@ -31,12 +30,6 @@ pub enum AppEvent { id: String, result: Result, String>, }, - Collections { - id: u64, - title: String, - result: Result, String>, - }, - Media(Cmd), } const FIRST_PAGE: usize = 20; @@ -68,7 +61,6 @@ pub enum Mode { Normal, Command, ArtistPick, - CollectionPick, ConfirmDelete, Help, Menu, @@ -107,8 +99,6 @@ pub enum MenuAction { PlayNext, Queue, Artist, - Similar, - Album, PlaylistMenu, PickPlaylist(String), NewPlaylist, @@ -176,9 +166,6 @@ pub struct App { pub artist_opts: Vec, pub info_track: Option, pub artist_sel: usize, - pub picks: Vec, - pub pick_sel: usize, - pub pick_title: String, pub pending_delete: Option, pub help_scroll: usize, pub lyrics: LyricsState, @@ -198,8 +185,6 @@ pub struct App { pub repeat: RepeatMode, pub accent: ratatui::style::Color, played: HashSet, - radio_played: HashSet, - resume_radio: bool, fail_streak: u8, result_cursor: Option, buffered: Vec, @@ -208,8 +193,6 @@ pub struct App { results_gen: u64, radio: Vec, on_radio: bool, - similar_load: Option, - mpris: Option, seek_step: f64, volume_step: f64, autoplay: bool, @@ -242,12 +225,6 @@ impl App { .map(|d| d.as_nanos() as u64) .unwrap_or(0x9E37_79B9_7F4A_7C15) | 1; - let mpris = { - let t = tx.clone(); - Mpris::new(move |c| { - let _ = t.send(AppEvent::Media(c)); - }) - }; let mut app = App { view: View::Results, mode: Mode::Normal, @@ -271,9 +248,6 @@ impl App { artist_opts: Vec::new(), info_track: None, artist_sel: 0, - picks: Vec::new(), - pick_sel: 0, - pick_title: String::new(), pending_delete: None, help_scroll: 0, lyrics: LyricsState::Idle, @@ -292,8 +266,6 @@ impl App { repeat: p.repeat, accent: cfg.ui.accent, played: HashSet::new(), - radio_played: HashSet::new(), - resume_radio: false, fail_streak: 0, result_cursor: None, buffered: Vec::new(), @@ -302,8 +274,6 @@ impl App { results_gen: 0, radio: Vec::new(), on_radio: false, - similar_load: None, - mpris, seek_step: p.seek_step, volume_step: p.volume_step, autoplay: p.autoplay, @@ -345,7 +315,6 @@ impl App { eprintln!("drift: could not save session: {e:#}"); } self.player = None; - self.mpris = None; } fn save_session(&mut self, force: bool) -> Result<()> { @@ -389,7 +358,6 @@ impl App { self.tick = self.tick.wrapping_add(1); self.load_more(); self.sync_lyrics(); - self.sync_mpris(); if self.tick.is_multiple_of(50) { match self.save_session(false) { Ok(()) => self.save_failed = false, @@ -492,7 +460,6 @@ impl App { Mode::Help => self.key_help(key), Mode::Command => self.key_command(key), Mode::ArtistPick => self.key_artist_pick(key), - Mode::CollectionPick => self.key_collection_pick(key), Mode::ConfirmDelete => self.key_confirm_delete(key), Mode::Menu => self.key_menu(key), Mode::Info => self.key_info(key), @@ -517,8 +484,6 @@ impl App { ('g', 'p') => self.set_view(View::Playlists), ('d', 'd') => self.delete_selected(), ('g', 'a') => self.go_to_artist(), - ('g', 's') => self.go_similar(), - ('g', 'b') => self.go_album(), _ => {} } } @@ -661,10 +626,6 @@ impl App { items.push(menu_item('a', "Add to queue", MenuAction::Queue)); } items.push(menu_item('g', "Show artist", MenuAction::Artist)); - items.push(menu_item('s', "Similar songs", MenuAction::Similar)); - if track.album.is_some() { - items.push(menu_item('b', "Show album", MenuAction::Album)); - } items.push(menu_item('l', "Add to playlist…", MenuAction::PlaylistMenu)); if in_queue { items.push(menu_item('x', "Remove from queue", MenuAction::Remove)); @@ -780,8 +741,6 @@ impl App { } MenuAction::Queue | MenuAction::AppendPlaylist => self.add_selected(), MenuAction::Artist => self.go_to_artist(), - MenuAction::Similar => self.go_similar(), - MenuAction::Album => self.go_album(), MenuAction::PlaylistMenu => {} MenuAction::PickPlaylist(name) => self.add_to_playlist(&name), MenuAction::NewPlaylist => self.prefill_command("addto "), @@ -1119,10 +1078,6 @@ impl App { Command::AddTo(name) => self.add_to_playlist(&name), Command::Import(url) => self.start_import(url), Command::Artist => self.go_to_artist(), - Command::Similar => self.go_similar(), - Command::Album(Some(q)) => self.start_collection_search(q, Kind::Album), - Command::Album(None) => self.go_album(), - Command::PlaylistSearch(q) => self.start_collection_search(q, Kind::Playlist), Command::Lyrics => self.lyrics_panel = !self.lyrics_panel, Command::Help => self.open_help(), } @@ -1227,127 +1182,6 @@ impl App { }); } - fn go_album(&mut self) { - let Some(track) = self.artist_target() else { - return self.error("No song selected"); - }; - let Some(album) = track.album.clone() else { - return self.error("No album known for this song"); - }; - let artist = split_artists(&track.artist) - .into_iter() - .next() - .unwrap_or_default(); - let busy = format!("Loading album \u{201c}{album}\u{201d}"); - let title = format!("Album: {album}"); - self.spawn_loader(title, busy, move || { - let found = ytmusic::search_collections(&format!("{album} {artist}"), Kind::Album)?; - let want = album.to_lowercase(); - let pick = found - .iter() - .find(|c| c.title.to_lowercase() == want) - .or(found.first()) - .ok_or_else(|| "album not found".to_string())?; - ytmusic::browse_tracks(&pick.browse_id, &pick.title, &pick.artist).map(Page::from) - }); - } - - fn start_collection_search(&mut self, query: String, kind: Kind) { - let (what, title) = match kind { - Kind::Album => ("albums", format!("Albums: {query}")), - Kind::Playlist => ("playlists", format!("Playlists: {query}")), - }; - self.search_id += 1; - let id = self.search_id; - self.busy = Some(format!("Searching {what} for \u{201c}{query}\u{201d}")); - let tx = self.tx.clone(); - std::thread::spawn(move || { - let result = ytmusic::search_collections(&query, kind); - let _ = tx.send(AppEvent::Collections { id, title, result }); - }); - } - - fn key_collection_pick(&mut self, key: KeyEvent) { - let len = self.picks.len(); - if len == 0 { - self.mode = Mode::Normal; - return; - } - match key.code { - KeyCode::Esc | KeyCode::Char('q') => self.close_collection_pick(), - KeyCode::Char('j') | KeyCode::Down => self.pick_sel = (self.pick_sel + 1).min(len - 1), - KeyCode::Char('k') | KeyCode::Up => self.pick_sel = self.pick_sel.saturating_sub(1), - KeyCode::Char('g') | KeyCode::Home => self.pick_sel = 0, - KeyCode::Char('G') | KeyCode::End => self.pick_sel = len - 1, - KeyCode::Char(c @ '1'..='9') => { - let i = c as usize - '1' as usize; - if i < len { - self.pick_sel = i; - self.confirm_collection(); - } - } - KeyCode::Enter => self.confirm_collection(), - _ => {} - } - } - - fn close_collection_pick(&mut self) { - self.mode = Mode::Normal; - self.picks.clear(); - self.pick_sel = 0; - } - - fn confirm_collection(&mut self) { - let c = self.picks.get(self.pick_sel).cloned(); - self.close_collection_pick(); - let Some(c) = c else { return }; - let kind = if c.browse_id.starts_with("VL") { "Playlist" } else { "Album" }; - let busy = format!("Loading \u{201c}{}\u{201d}", c.title); - let title = format!("{kind}: {}", c.title); - self.spawn_loader(title, busy, move || { - ytmusic::browse_tracks(&c.browse_id, &c.title, &c.artist).map(Page::from) - }); - } - - fn on_media(&mut self, cmd: Cmd) { - match cmd { - Cmd::Play => match self.play_state { - PlayState::Stopped => self.play_default(), - _ => self.set_pause(false), - }, - Cmd::Pause => { - if self.play_state != PlayState::Stopped { - self.set_pause(true); - } - } - Cmd::Toggle => self.toggle_pause(), - Cmd::Stop => self.stop_playback(), - Cmd::Next => self.next(), - Cmd::Prev => self.prev(), - Cmd::SeekBy(d) => self.seek(Amount::Delta(d)), - Cmd::SeekTo(p) => self.seek(Amount::Abs(p)), - Cmd::Quit => self.should_quit = true, - } - } - - fn sync_mpris(&mut self) { - if let Some(mut m) = self.mpris.take() { - m.update(self.now_track(), self.play_state, self.position); - self.mpris = Some(m); - } - } - - fn go_similar(&mut self) { - let Some(track) = self.artist_target() else { - return self.error("No song selected"); - }; - let busy = format!("Finding songs like \u{201c}{}\u{201d}", track.title); - let title = format!("Similar to {}", track.title); - let id = track.id; - self.spawn_loader(title, busy, move || ytmusic::radio(&id).map(Page::from)); - self.similar_load = Some(self.search_id); - } - fn start_search(&mut self, query: String) { self.spawn_loader( format!("Search: {query}"), @@ -1385,7 +1219,6 @@ impl App { return; } self.busy = None; - let autoplay = self.similar_load.take() == Some(id); match result { Ok(page) if page.tracks.is_empty() => self.info("No results found."), Ok(page) => { @@ -1403,9 +1236,6 @@ impl App { self.states[View::Results.idx()].select(Some(0)); self.set_view(View::Results); self.info(format!("{n} results — Enter plays, a queues, ? for help")); - if autoplay { - self.play_result(0); - } } Err(e) => self.error(e), } @@ -1438,23 +1268,6 @@ impl App { } } } - AppEvent::Collections { id, title, result } => { - if id != self.search_id { - return; - } - self.busy = None; - match result { - Ok(list) if list.is_empty() => self.info("Nothing found."), - Ok(list) => { - self.picks = list; - self.pick_sel = 0; - self.pick_title = title; - self.mode = Mode::CollectionPick; - } - Err(e) => self.error(e), - } - } - AppEvent::Media(cmd) => self.on_media(cmd), AppEvent::Lyrics { id, result } => { if self.lyrics_id.as_deref() != Some(id.as_str()) { return; @@ -1643,7 +1456,6 @@ impl App { } else { self.current.unwrap_or(0) }; - self.resume_radio = false; self.start_track(i.min(self.queue.len() - 1), true); } @@ -1661,7 +1473,6 @@ impl App { } View::Queue => { if i < self.queue.len() { - self.resume_radio = false; self.play_index(i); } } @@ -1671,7 +1482,7 @@ impl App { self.play_playlist_from(&name, i); } } else if let Some(name) = self.playlist_name(i) { - self.play_playlist_from(&name, 0); + self.load_playlist(&name, true); } } } @@ -1682,7 +1493,6 @@ impl App { return; } self.radio = self.results.clone(); - self.radio_played.clear(); self.play_radio(i); } @@ -1691,9 +1501,7 @@ impl App { return; }; self.on_radio = true; - self.resume_radio = true; self.result_cursor = Some(i); - self.radio_played.insert(i); self.resume = None; self.play_state = PlayState::Loading; self.position = 0.0; @@ -1796,12 +1604,11 @@ impl App { } let len = self.queue.len(); - let wrap = self.repeat == RepeatMode::All && !(self.on_radio || self.resume_radio); let next = if len == 0 { None } else if self.shuffle { let mut pool: Vec = (0..len).filter(|i| !self.played.contains(i)).collect(); - if pool.is_empty() && wrap { + if pool.is_empty() && self.repeat == RepeatMode::All { self.played.clear(); pool = (0..len).filter(|i| Some(*i) != self.current).collect(); if pool.is_empty() { @@ -1817,7 +1624,7 @@ impl App { } else { match self.current { Some(c) if c + 1 < len => Some(c + 1), - Some(_) if wrap => Some(0), + Some(_) if self.repeat == RepeatMode::All => Some(0), Some(_) => None, None => Some(0), } @@ -1829,45 +1636,21 @@ impl App { } fn advance_radio(&mut self, auto: bool) { - let cursor = self + let next = self .result_cursor - .filter(|_| self.on_radio || self.resume_radio || self.autoplay); - match cursor.and_then(|c| self.next_in_radio(c)) { + .filter(|_| self.on_radio || self.autoplay) + .map(|c| c + 1) + .filter(|&i| i < self.radio.len()); + match next { Some(i) => self.play_radio(i), None if auto => { self.stop_playback(); - self.info("Finished playing"); + self.info("Queue finished"); } - None => self.info("End of the list"), + None => self.info("End of queue"), } } - fn next_in_radio(&mut self, cursor: usize) -> Option { - let len = self.radio.len(); - if !self.shuffle { - return match cursor + 1 { - n if n < len => Some(n), - _ if self.repeat == RepeatMode::All && len > 0 => Some(0), - _ => None, - }; - } - let mut pool: Vec = (0..len) - .filter(|i| !self.radio_played.contains(i)) - .collect(); - if pool.is_empty() && self.repeat == RepeatMode::All { - self.radio_played.clear(); - pool = (0..len).filter(|&i| i != cursor).collect(); - if pool.is_empty() { - pool = vec![cursor]; - } - } - if pool.is_empty() { - return None; - } - let k = self.rand_below(pool.len()); - Some(pool[k]) - } - fn seek(&mut self, amount: Amount) { if !matches!(self.play_state, PlayState::Playing | PlayState::Paused) { return self.error("Nothing is playing"); @@ -1909,10 +1692,6 @@ impl App { if let Some(c) = self.current { self.played.insert(c); } - self.radio_played.clear(); - if let Some(c) = self.result_cursor { - self.radio_played.insert(c); - } self.info(format!( "Shuffle: {}", if self.shuffle { "on" } else { "off" } @@ -1975,7 +1754,11 @@ impl App { } else { 0 }; - self.play_result(start.min(self.results.len() - 1)); + self.queue = self.results.clone(); + self.played.clear(); + self.current = None; + self.result_cursor = None; + self.play_index(start.min(self.queue.len() - 1)); } fn clear_queue(&mut self) { @@ -2126,7 +1909,6 @@ impl App { self.played.clear(); self.current = None; self.result_cursor = None; - self.resume_radio = false; self.states[View::Queue.idx()].select(Some(0)); self.play_index(0); self.info(format!( @@ -2161,9 +1943,12 @@ impl App { return self.error(format!("Playlist \u{201c}{name}\u{201d} is empty")); } let i = i.min(tracks.len() - 1); - self.radio = tracks; - self.radio_played.clear(); - self.play_radio(i); + self.queue = tracks; + self.played.clear(); + self.current = None; + self.result_cursor = None; + self.states[View::Queue.idx()].select(Some(i)); + self.play_index(i); } fn remove_playlist_track(&mut self, i: usize) { @@ -2295,61 +2080,6 @@ mod tests { assert_eq!(app.results.len(), 25); } - fn now_title(app: &App) -> Option<&str> { - app.now_track().map(|t| t.title.as_str()) - } - - fn check_playlist_playback(app: &mut App) { - app.queue.clear(); - app.current = None; - app.played.clear(); - app.store.playlists.insert( - "run".into(), - vec![ - track("rrrrrrrrrr1", "R1"), - track("rrrrrrrrrr2", "R2"), - track("rrrrrrrrrr3", "R3"), - ], - ); - - app.set_view(View::Playlists); - app.open = Some("run".into()); - app.pl_state.select(Some(0)); - press(app, KeyCode::Enter); - assert!(app.queue.is_empty()); - assert_eq!(now_title(app), Some("R1")); - - app.enqueue(track("qqqqqqqqqq1", "Q1")); - app.advance(true); - assert_eq!(now_title(app), Some("Q1")); - app.advance(true); - assert_eq!(now_title(app), Some("R2")); - app.advance(true); - assert_eq!(now_title(app), Some("R3")); - assert_eq!(app.queue.len(), 1); - - app.repeat = RepeatMode::All; - app.advance(true); - assert_eq!(now_title(app), Some("R1")); - app.repeat = RepeatMode::Off; - - app.open = None; - let i = app.store.playlists.keys().position(|k| k == "run").unwrap(); - app.activate_at(View::Playlists, i); - assert_eq!(now_title(app), Some("R1")); - assert_eq!(app.queue.len(), 1); - - app.shuffle = true; - app.advance(true); - let first = app.result_cursor.unwrap(); - app.advance(true); - let second = app.result_cursor.unwrap(); - assert!(first != 0 && second != 0 && first != second); - app.advance(true); - assert_eq!(app.result_cursor, Some(second)); - app.shuffle = false; - } - #[test] fn playlist_drill_in() { let dir = std::env::temp_dir().join(format!("drift-test-{}", std::process::id())); @@ -2444,8 +2174,6 @@ mod tests { app.run_command("remove"); assert_eq!(app.queue.len(), 1); - check_playlist_playback(&mut app); - let _ = std::fs::remove_dir_all(&dir); } } diff --git a/src/command.rs b/src/command.rs index 815e1fc..9237286 100644 --- a/src/command.rs +++ b/src/command.rs @@ -36,9 +36,6 @@ pub enum Command { AddTo(String), Import(String), Artist, - Similar, - Album(Option), - PlaylistSearch(String), Lyrics, Help, } @@ -72,9 +69,6 @@ pub const COMMAND_NAMES: &[&str] = &[ "addto", "import", "artist", - "similar", - "album", - "playlist", "lyrics", "help", ]; @@ -183,14 +177,6 @@ pub fn parse(line: &str) -> Result, String> { Import(arg.to_string()) } "artist" | "ar" => Artist, - "similar" | "radio" | "sim" => Similar, - "album" | "albums" | "al" => Album((!arg.is_empty()).then(|| arg.to_string())), - "playlist" => { - if arg.is_empty() { - return Err("usage: :playlist \"query\"".into()); - } - PlaylistSearch(arg.to_string()) - } "lyrics" | "ly" => Lyrics, "help" | "h" => Help, other => return Err(format!("unknown command: :{other} (try :help)")), @@ -256,19 +242,6 @@ mod tests { assert_eq!(parse("artist"), Ok(Some(Command::Artist))); assert_eq!(parse(":ar"), Ok(Some(Command::Artist))); assert_eq!(parse("lyrics"), Ok(Some(Command::Lyrics))); - assert_eq!(parse("similar"), Ok(Some(Command::Similar))); - assert_eq!(parse(":radio"), Ok(Some(Command::Similar))); - assert_eq!(parse("album"), Ok(Some(Command::Album(None)))); - assert_eq!( - parse("album daft punk"), - Ok(Some(Command::Album(Some("daft punk".into())))) - ); - assert_eq!( - parse("playlist lofi"), - Ok(Some(Command::PlaylistSearch("lofi".into()))) - ); - assert!(parse("playlist").is_err()); - assert_eq!(parse("playlists"), Ok(Some(Command::ShowPlaylists))); } #[test] diff --git a/src/main.rs b/src/main.rs index 5a44a69..aca55f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ mod command; mod config; mod lyrics; mod model; -mod mpris; mod player; mod session; mod store; @@ -129,12 +128,6 @@ fn run(initial_query: Option) -> Result<()> { } let mut terminal = ratatui::try_init()?; - let hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - if std::thread::current().name() == Some("main") { - hook(info); - } - })); let result = event_loop(&mut terminal, &mut app, &rx); ratatui::restore(); app.shutdown(); diff --git a/src/model.rs b/src/model.rs index 0f7a156..685c42c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -21,14 +21,6 @@ impl Track { } } -#[derive(Debug, Clone, PartialEq)] -pub struct Collection { - pub browse_id: String, - pub title: String, - pub subtitle: String, - pub artist: String, -} - pub fn split_artists(credit: &str) -> Vec { const MARK: char = '\u{1}'; let mut s = credit.to_string(); diff --git a/src/mpris.rs b/src/mpris.rs deleted file mode 100644 index 31c47cf..0000000 --- a/src/mpris.rs +++ /dev/null @@ -1,162 +0,0 @@ -/* MPRIS (media keys, playerctl, waybar...) on Linux; a no-op elsewhere. */ - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cmd { - Play, - Pause, - Toggle, - Stop, - Next, - Prev, - SeekBy(f64), - SeekTo(f64), - Quit, -} - -#[cfg(target_os = "linux")] -mod imp { - use std::time::Duration; - - use souvlaki::{ - MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, - PlatformConfig, SeekDirection, - }; - - use super::Cmd; - use crate::app::PlayState; - use crate::model::Track; - - pub struct Mpris { - controls: MediaControls, - track: Option, - state: PlayState, - secs: u64, - } - - impl Mpris { - /* None when there is no session bus. */ - pub fn new(send: impl Fn(Cmd) + Send + 'static) -> Option { - let name = format!("drift.instance{}", std::process::id()); - let config = PlatformConfig { - dbus_name: &name, - display_name: "drift", - hwnd: None, - }; - let mut controls = MediaControls::new(config).ok()?; - controls - .attach(move |ev| { - if let Some(c) = convert(ev) { - send(c); - } - }) - .ok()?; - Some(Mpris { - controls, - track: None, - state: PlayState::Stopped, - secs: 0, - }) - } - - pub fn update(&mut self, track: Option<&Track>, state: PlayState, position: f64) { - let id = track.map(|t| t.id.as_str()); - if id != self.track.as_deref() { - self.track = id.map(str::to_string); - let cover = track.map(|t| format!("https://i.ytimg.com/vi/{}/hqdefault.jpg", t.id)); - let meta = match track { - Some(t) => MediaMetadata { - title: Some(t.title.as_str()), - album: t.album.as_deref(), - artist: Some(t.artist.as_str()), - cover_url: cover.as_deref(), - duration: t.duration.map(|d| Duration::from_secs(u64::from(d))), - }, - None => MediaMetadata::default(), - }; - let _ = self.controls.set_metadata(meta); - self.secs = u64::MAX; - } - let pos = if position.is_finite() { position.max(0.0) } else { 0.0 }; - let secs = pos as u64; - if state == self.state && secs == self.secs { - return; - } - self.state = state; - self.secs = secs; - let progress = Some(MediaPosition(Duration::from_secs_f64(pos))); - let _ = self.controls.set_playback(match state { - PlayState::Playing | PlayState::Loading => MediaPlayback::Playing { progress }, - PlayState::Paused => MediaPlayback::Paused { progress }, - PlayState::Stopped => MediaPlayback::Stopped, - }); - } - } - - fn dir(d: SeekDirection, secs: f64) -> f64 { - if d == SeekDirection::Backward { - -secs - } else { - secs - } - } - - pub(super) fn convert(ev: MediaControlEvent) -> Option { - Some(match ev { - MediaControlEvent::Play => Cmd::Play, - MediaControlEvent::Pause => Cmd::Pause, - MediaControlEvent::Toggle => Cmd::Toggle, - MediaControlEvent::Next => Cmd::Next, - MediaControlEvent::Previous => Cmd::Prev, - MediaControlEvent::Stop => Cmd::Stop, - MediaControlEvent::Seek(d) => Cmd::SeekBy(dir(d, 5.0)), - MediaControlEvent::SeekBy(d, by) => Cmd::SeekBy(dir(d, by.as_secs_f64())), - MediaControlEvent::SetPosition(MediaPosition(p)) => Cmd::SeekTo(p.as_secs_f64()), - MediaControlEvent::Quit => Cmd::Quit, - _ => return None, - }) - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn maps_events() { - assert_eq!(convert(MediaControlEvent::Previous), Some(Cmd::Prev)); - assert_eq!( - convert(MediaControlEvent::SeekBy( - SeekDirection::Backward, - Duration::from_secs(10) - )), - Some(Cmd::SeekBy(-10.0)) - ); - assert_eq!( - convert(MediaControlEvent::SetPosition(MediaPosition( - Duration::from_secs(90) - ))), - Some(Cmd::SeekTo(90.0)) - ); - assert_eq!(convert(MediaControlEvent::Raise), None); - } - } -} - -#[cfg(not(target_os = "linux"))] -mod imp { - use super::Cmd; - use crate::app::PlayState; - use crate::model::Track; - - pub struct Mpris; - - impl Mpris { - pub fn new(_send: impl Fn(Cmd) + Send + 'static) -> Option { - None - } - - pub fn update(&mut self, _track: Option<&Track>, _state: PlayState, _position: f64) {} - } -} - -pub use imp::Mpris; diff --git a/src/ui.rs b/src/ui.rs index a4321db..c3cfa98 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -69,9 +69,6 @@ pub fn draw(f: &mut Frame, app: &mut App) { if app.mode == Mode::ArtistPick { draw_artist_picker(f, app, area); } - if app.mode == Mode::CollectionPick { - draw_collection_picker(f, app, area); - } if app.mode == Mode::Info { draw_info(f, app, area); } @@ -804,9 +801,6 @@ const CMD_INFO: &[(&str, &str)] = &[ ("addto", "add the selected track to a playlist"), ("import", "import a YouTube / YT Music playlist URL"), ("artist", "songs by the selected artist"), - ("similar", "radio: songs similar to the selected one"), - ("album", "open the selected song's album, or :album to search"), - ("playlist", "search YouTube Music playlists"), ("lyrics", "show or hide the lyrics panel"), ("help", "show key bindings"), ]; @@ -1081,19 +1075,10 @@ const HELP: &[(&str, &str)] = &[ "ga or :artist", "songs by the artist of the selected song; a popup asks which one when there are several", ), - ( - "gb or :album", - "open the album of the selected song; :album searches albums", - ), - (":playlist ", "search YouTube Music playlists (pick one, then ⏎)"), - ( - "gs or :similar", - "radio: load songs similar to the selected one and start playing them", - ), ("SONG MENU ( G )", "#"), ("G", "menu for the highlighted song or playlist"), ("j k ⏎", "move / choose (the yellow letter is a shortcut)"), - ("p n a g s b l", "play · next · queue · artist · similar · album · playlist"), + ("p n a g l", "play · play next · add to queue · artist · playlist"), ("x i", "remove · song info"), ("Esc q G", "close (Esc goes back from a sub-menu)"), ("QUEUE", "#"), @@ -1131,8 +1116,6 @@ const HELP: &[(&str, &str)] = &[ (":addto n", "add the selected track to playlist n"), (":import URL", "load a YouTube / YT Music playlist"), (":artist", "same as ga"), - (":similar", "same as gs (also :radio)"), - (":album [q]", "same as gb, or search albums"), (":lyrics", "same as L"), (":quit (or q, Ctrl-c)", ""), ]; @@ -1236,55 +1219,6 @@ fn draw_artist_picker(f: &mut Frame, app: &App, area: Rect) { ); } -fn draw_collection_picker(f: &mut Frame, app: &App, area: Rect) { - let n = app.picks.len(); - let w = area.width.min(78); - let h = (n as u16).saturating_add(3).min(area.height); - let visible = h.saturating_sub(3) as usize; - let start = if app.pick_sel < visible { - 0 - } else { - app.pick_sel + 1 - visible - }; - let popup = Rect { - x: area.x + (area.width - w) / 2, - y: area.y + (area.height - h) / 2, - width: w, - height: h, - }; - let inner = w.saturating_sub(2) as usize; - let mut lines: Vec = app - .picks - .iter() - .enumerate() - .skip(start) - .take(visible) - .map(|(i, c)| { - let mut text = format!(" {:>2} {}", i + 1, c.title); - if !c.subtitle.is_empty() { - text.push_str(&format!(" · {}", c.subtitle)); - } - let text: String = text.chars().take(inner).collect(); - if i == app.pick_sel { - let pad = inner.saturating_sub(text.chars().count()); - Line::styled(format!("{text}{}", " ".repeat(pad)), selected_style(app.accent)) - } else { - Line::raw(text) - } - }) - .collect(); - lines.push(Line::styled(" Enter select · j k move · Esc cancel", dim())); - f.render_widget(Clear, popup); - f.render_widget( - Paragraph::new(lines).block( - Block::bordered() - .title(format!(" {} ", app.pick_title)) - .border_style(Style::new().fg(Color::Yellow)), - ), - popup, - ); -} - fn draw_help(f: &mut Frame, app: &mut App, area: Rect) { let w = area.width.min(80); let lines = help_lines(w.saturating_sub(2) as usize, app.accent); diff --git a/src/ytmusic.rs b/src/ytmusic.rs index 97dd7f1..203f9f2 100644 --- a/src/ytmusic.rs +++ b/src/ytmusic.rs @@ -4,26 +4,15 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, bail, Context, Result}; use serde_json::{json, Value}; -use crate::model::{is_valid_id, parse_clock, Collection, Track}; +use crate::model::{is_valid_id, parse_clock, Track}; const ENDPOINT: &str = "https://music.youtube.com/youtubei/v1/search?prettyPrint=false"; const BROWSE_ENDPOINT: &str = "https://music.youtube.com/youtubei/v1/browse?prettyPrint=false"; -const NEXT_ENDPOINT: &str = "https://music.youtube.com/youtubei/v1/next?prettyPrint=false"; -const ALBUMS_FILTER: &str = "EgWKAQIYAWoMEA4QChADEAQQCRAF"; -const PLAYLISTS_FILTER: &str = "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D"; -const RADIO_TARGET: usize = 50; -const RADIO_PAGES: usize = 4; const SONGS_FILTER: &str = "EgWKAQIIAWoMEA4QChADEAQQCRAF"; const ARTISTS_FILTER: &str = "EgWKAQIgAWoMEA4QChADEAQQCRAF"; const UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"; const FALLBACK_COUNT: usize = 25; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Kind { - Album, - Playlist, -} - pub struct Page { pub tracks: Vec, pub next: Option, @@ -60,91 +49,6 @@ pub fn search_more(token: &str) -> Result { .map_err(|e| format!("{e:#}")) } -pub fn search_collections(query: &str, kind: Kind) -> Result, String> { - let params = match kind { - Kind::Album => ALBUMS_FILTER, - Kind::Playlist => PLAYLISTS_FILTER, - }; - post(ENDPOINT, json!({ "query": query, "params": params })) - .map(|v| parse_collections(&v, kind)) - .map_err(|e| format!("{e:#}")) -} - -/* browse_id: MPREb_... for an album, VL... for a playlist */ -pub fn browse_tracks(browse_id: &str, album: &str, artist: &str) -> Result, String> { - let playlist = browse_id.starts_with("VL"); - let v = post(BROWSE_ENDPOINT, json!({ "browseId": browse_id })).map_err(|e| format!("{e:#}"))?; - let mut tracks = parse_browse(&v, playlist); - let mut next = if playlist { find_continuation(&v) } else { None }; - let mut pages = 0; - while let Some(tok) = next.take() { - if pages >= 5 || tracks.len() >= 300 { - break; - } - pages += 1; - let url = format!("{BROWSE_ENDPOINT}&ctoken={tok}&continuation={tok}"); - let Ok(v) = post(&url, json!({})) else { break }; - let more = parse_browse(&v, true); - if more.is_empty() { - break; - } - let known: std::collections::HashSet = - tracks.iter().map(|t| t.id.clone()).collect(); - tracks.extend(more.into_iter().filter(|t| !known.contains(&t.id))); - next = find_continuation(&v); - } - if tracks.is_empty() { - return Err("nothing playable found".into()); - } - if !playlist { - for t in &mut tracks { - if !album.is_empty() && t.album.is_none() { - t.album = Some(album.to_string()); - } - if !artist.is_empty() && t.artist == "Unknown artist" { - t.artist = artist.to_string(); - } - } - } - Ok(tracks) -} - -pub fn radio(seed: &str) -> Result, String> { - fetch_radio(seed).map_err(|e| format!("{e:#}")) -} - -fn fetch_radio(seed: &str) -> Result> { - let body = json!({ - "videoId": seed, - "playlistId": format!("RDAMVM{seed}"), - "params": "wAEB", - "isAudioOnly": true, - "enablePersistentPlaylistPanel": true, - "tunerSettingValue": "AUTOMIX_SETTING_NORMAL", - }); - let mut url = NEXT_ENDPOINT.to_string(); - let mut tracks: Vec = Vec::new(); - for _ in 0..RADIO_PAGES { - let v = post(&url, body.clone())?; - let before = tracks.len(); - for t in parse_radio_response(&v) { - if !tracks.iter().any(|x| x.id == t.id) { - tracks.push(t); - } - } - match find_continuation(&v) { - Some(tok) if tracks.len() < RADIO_TARGET && tracks.len() > before => { - url = format!("{NEXT_ENDPOINT}&ctoken={tok}&continuation={tok}"); - } - _ => break, - } - } - if tracks.is_empty() { - bail!("no similar songs found"); - } - Ok(tracks) -} - pub fn import(url: &str) -> Result, String> { if !(url.starts_with("http://") || url.starts_with("https://")) { return Err("import expects a full http(s) URL".into()); @@ -215,7 +119,6 @@ fn find_continuation(v: &Value) -> Option { Value::Object(map) => { let token = map .get("nextContinuationData") - .or_else(|| map.get("nextRadioContinuationData")) .and_then(|c| c.get("continuation")) .or_else(|| { map.get("continuationCommand") @@ -317,15 +220,11 @@ fn find_str<'a>(v: &'a Value, key: &str) -> Option<&'a str> { } fn column_segments(item: &Value, col: usize) -> Vec { - run_segments( - item.get("flexColumns") - .and_then(|c| c.get(col)) - .and_then(|c| c.pointer("/musicResponsiveListItemFlexColumnRenderer/text/runs")) - .and_then(Value::as_array), - ) -} - -fn run_segments(runs: Option<&Vec>) -> Vec { + let runs = item + .get("flexColumns") + .and_then(|c| c.get(col)) + .and_then(|c| c.pointer("/musicResponsiveListItemFlexColumnRenderer/text/runs")) + .and_then(Value::as_array); let mut segments = Vec::new(); let mut cur = String::new(); for run in runs.into_iter().flatten() { @@ -350,13 +249,6 @@ fn looks_like_duration(s: &str) -> bool { s.contains(':') && s.chars().all(|c| c.is_ascii_digit() || c == ':') } -fn fixed_duration(item: &Value) -> Option { - let s = item - .pointer("/fixedColumns/0/musicResponsiveListItemFixedColumnRenderer/text/runs/0/text")? - .as_str()?; - parse_clock(s).map(|s| s as u32) -} - fn parse_item(item: &Value) -> Option { let id = find_str(item, "videoId")?; if !is_valid_id(id) { @@ -379,7 +271,6 @@ fn parse_item(item: &Value) -> Option { } _ => None, }; - let duration = duration.or_else(|| fixed_duration(item)); let artist = meta .first() .cloned() @@ -398,63 +289,6 @@ fn parse_item(item: &Value) -> Option { }) } -fn parse_browse(v: &Value, playlist: bool) -> Vec { - let mut items = Vec::new(); - collect_items(v, &mut items); - let mut seen = std::collections::HashSet::new(); - items - .into_iter() - .filter_map(|item| { - let mut t = parse_item(item)?; - if playlist && t.album.is_none() { - t.album = column_segments(item, 2).into_iter().next(); - } - Some(t) - }) - .filter(|t| seen.insert(t.id.clone())) - .collect() -} - -pub fn parse_collections(v: &Value, kind: Kind) -> Vec { - let mut items = Vec::new(); - collect_items(v, &mut items); - let mut seen = std::collections::HashSet::new(); - items - .into_iter() - .filter_map(|item| { - let id = item - .pointer("/navigationEndpoint/browseEndpoint/browseId")? - .as_str()?; - let ok = match kind { - Kind::Album => id.starts_with("MPRE"), - Kind::Playlist => id.starts_with("VL"), - }; - if !ok { - return None; - } - let title = column_segments(item, 0).into_iter().next()?; - let mut meta = column_segments(item, 1); - if matches!( - meta.first().map(String::as_str), - Some("Album" | "EP" | "Single" | "Playlist") - ) { - meta.remove(0); - } - let artist = match kind { - Kind::Album => meta.first().cloned().unwrap_or_default(), - Kind::Playlist => String::new(), - }; - Some(Collection { - browse_id: id.to_string(), - title, - subtitle: meta.join(" · "), - artist, - }) - }) - .filter(|c| seen.insert(c.browse_id.clone())) - .collect() -} - pub fn parse_search_response(v: &Value) -> Vec { let mut items = Vec::new(); collect_items(v, &mut items); @@ -466,76 +300,6 @@ pub fn parse_search_response(v: &Value) -> Vec { .collect() } -fn collect_panel<'a>(v: &'a Value, out: &mut Vec<&'a Value>) { - match v { - Value::Object(map) => { - if let Some(w) = map.get("playlistPanelVideoWrapperRenderer") { - out.extend(w.pointer("/primaryRenderer/playlistPanelVideoRenderer")); - } else if let Some(item) = map.get("playlistPanelVideoRenderer") { - out.push(item); - } else { - map.values().for_each(|c| collect_panel(c, out)); - } - } - Value::Array(arr) => arr.iter().for_each(|c| collect_panel(c, out)), - _ => {} - } -} - -fn parse_panel_item(item: &Value) -> Option { - let id = item.get("videoId")?.as_str()?; - if !is_valid_id(id) { - return None; - } - let title: String = item - .pointer("/title/runs")? - .as_array()? - .iter() - .filter_map(|r| r.get("text")?.as_str()) - .collect(); - let title = title.trim().to_string(); - if title.is_empty() { - return None; - } - let meta = run_segments(item.pointer("/longBylineText/runs").and_then(Value::as_array)); - let artist = meta - .first() - .cloned() - .unwrap_or_else(|| "Unknown artist".into()); - let album = meta - .get(1) - .filter(|a| { - !a.contains(" views") - && !a.contains(" plays") - && !(a.len() == 4 && a.bytes().all(|b| b.is_ascii_digit())) - }) - .cloned(); - let duration = item - .pointer("/lengthText/runs/0/text") - .or_else(|| item.pointer("/lengthText/simpleText")) - .and_then(Value::as_str) - .and_then(parse_clock) - .map(|s| s as u32); - Some(Track { - id: id.to_string(), - title, - artist, - album, - duration, - }) -} - -pub fn parse_radio_response(v: &Value) -> Vec { - let mut items = Vec::new(); - collect_panel(v, &mut items); - let mut seen = std::collections::HashSet::new(); - items - .into_iter() - .filter_map(parse_panel_item) - .filter(|t| seen.insert(t.id.clone())) - .collect() -} - fn ytdlp_search(query: &str) -> Result> { ytdlp_entries(&format!("ytsearch{FALLBACK_COUNT}:{query}")) } @@ -699,76 +463,6 @@ not json assert_eq!(find_continuation(&json!({"a": [1, 2]})), None); } - #[test] - fn parses_radio_panel() { - let item = |id: &str, title: &str, by: Value, len: &str| { - json!({"playlistPanelVideoRenderer": { - "videoId": id, - "title": {"runs": [{"text": title}]}, - "longBylineText": {"runs": by}, - "lengthText": {"runs": [{"text": len}]}, - }}) - }; - let by = json!([{"text": "A"}, {"text": " & "}, {"text": "B"}, - {"text": " • "}, {"text": "Album"}, {"text": " • "}, {"text": "2019"}]); - let resp = json!({"contents": [ - item("abcdefghijk", "One", by, "3:21"), - {"playlistPanelVideoWrapperRenderer": { - "primaryRenderer": item("bcdefghijkl", "Two", json!([{"text": "C"}]), "1:05"), - "counterpart": [item("zzzzzzzzzzz", "Video", json!([{"text": "C"}]), "1:05")], - }}, - item("abcdefghijk", "dup", json!([{"text": "A"}]), "3:21"), - ]}); - let t = parse_radio_response(&resp); - assert_eq!(t.len(), 2); - assert_eq!(t[0].artist, "A & B"); - assert_eq!(t[0].album.as_deref(), Some("Album")); - assert_eq!(t[0].duration, Some(201)); - assert_eq!(t[1].id, "bcdefghijkl"); - assert_eq!(t[1].album, None); - } - - #[test] - fn parses_album_and_playlist_results() { - let row = |id: &str, title: &str, runs: Value| { - json!({"musicResponsiveListItemRenderer": { - "navigationEndpoint": {"browseEndpoint": {"browseId": id}}, - "flexColumns": [ - {"musicResponsiveListItemFlexColumnRenderer": run(title)}, - {"musicResponsiveListItemFlexColumnRenderer": {"text": {"runs": runs}}}, - ] - }}) - }; - let resp = json!([ - row("MPREb_abc", "Discovery", json!([{"text": "Album"}, {"text": " • "}, - {"text": "Daft Punk"}, {"text": " • "}, {"text": "2001"}])), - row("VLPL123", "Mix", json!([{"text": "Playlist"}, {"text": " • "}, {"text": "Someone"}])), - row("UCartist", "Artist", json!([{"text": "Artist"}])), - ]); - let a = parse_collections(&resp, Kind::Album); - assert_eq!(a.len(), 1); - assert_eq!(a[0].title, "Discovery"); - assert_eq!(a[0].artist, "Daft Punk"); - assert_eq!(a[0].subtitle, "Daft Punk · 2001"); - let p = parse_collections(&resp, Kind::Playlist); - assert_eq!(p.len(), 1); - assert_eq!(p[0].browse_id, "VLPL123"); - assert_eq!(p[0].artist, ""); - } - - #[test] - fn parses_browse_tracks() { - let resp = json!({"contents": [{"musicResponsiveListItemRenderer": { - "playlistItemData": {"videoId": "abcdefghijk"}, - "flexColumns": [{"musicResponsiveListItemFlexColumnRenderer": run("One More Time")}], - "fixedColumns": [{"musicResponsiveListItemFixedColumnRenderer": run("5:20")}], - }}]}); - let t = parse_browse(&resp, false); - assert_eq!(t.len(), 1); - assert_eq!(t[0].duration, Some(320)); - assert_eq!(t[0].artist, "Unknown artist"); - } - #[test] fn date_math() { assert_eq!(civil_from_days(0), (1970, 1, 1));