feat: browse individual saved channels

This commit is contained in:
2026-10-11 16:20:20 +02:00
parent f3e7e249be
commit b38ca30735
3 changed files with 687 additions and 179 deletions
+83
View File
@@ -27,6 +27,23 @@ impl Config {
channels.dedup();
channels.into_iter().cloned().collect()
}
pub fn saved_channels(&self) -> Vec<(String, String)> {
let mut labels = BTreeMap::new();
// Mappings are sorted by handle, so aliases use the first readable label.
for (handle, identity) in &self.channels {
labels.entry(identity).or_insert(handle);
}
for identity in self.categories.values().flatten() {
labels.entry(identity).or_insert(identity);
}
let mut channels: Vec<_> = labels
.into_iter()
.map(|(identity, label)| (label.clone(), identity.clone()))
.collect();
channels.sort_unstable();
channels
}
}
pub fn config_path() -> io::Result<PathBuf> {
@@ -1097,6 +1114,72 @@ mod tests {
);
}
#[test]
fn saved_channels_use_readable_aliases_and_include_every_subscription_once() {
let directory = TestDirectory::new();
let path = directory.0.join("config.toml");
let mapped_id = "UC4QobU6STFB0P71PMvOGN5A";
let untagged_id = "UCabcdefghijklmnopqrstuv";
let legacy_id = "UCzyxwvutsrqponmlkjihgfe";
let mapped = format!("https://www.youtube.com/channel/{mapped_id}");
let untagged = format!("https://www.youtube.com/channel/{untagged_id}");
let legacy = format!("https://www.youtube.com/channel/{legacy_id}");
fs::write(
&path,
format!(
"[channels]\n'@zAlias' = '{mapped_id}'\n'https://youtube.com/@aAlias' = '{mapped}'\n'@untagged' = '{untagged_id}'\n[categories]\nFirst = ['@zAlias', '{mapped_id}', '@manual', '{legacy_id}']\nSecond = ['@aAlias', 'https://youtube.com/@manual', '{legacy}']\n"
),
)
.unwrap();
let config = load(&path).unwrap();
let expected = vec![
("@aAlias".to_owned(), mapped.clone()),
("@untagged".to_owned(), untagged.clone()),
(
"https://www.youtube.com/@manual".to_owned(),
"https://www.youtube.com/@manual".to_owned(),
),
(legacy.clone(), legacy.clone()),
];
assert_eq!(config.saved_channels(), expected);
assert_eq!(load(&path).unwrap().saved_channels(), expected);
let mut identities: Vec<_> = config
.saved_channels()
.into_iter()
.map(|(_, identity)| identity)
.collect();
identities.sort_unstable();
assert_eq!(identities, config.subscriptions());
assert!(
config
.categories
.values()
.flatten()
.all(|id| id != &untagged)
);
assert_eq!(config.channels["@zAlias"], mapped);
}
#[test]
fn saved_channels_support_empty_and_registry_only_configs() {
assert!(Config::default().saved_channels().is_empty());
let directory = TestDirectory::new();
let path = directory.0.join("config.toml");
let identity = "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A";
fs::write(
&path,
format!("[channels]\n'@zAlias' = '{identity}'\n'@aAlias' = '{identity}'\n"),
)
.unwrap();
let config = load(&path).unwrap();
assert!(config.categories.is_empty());
assert_eq!(
config.saved_channels(),
vec![("@aAlias".to_owned(), identity.to_owned())]
);
assert_eq!(config.subscriptions(), [identity]);
}
#[test]
fn subscriptions_include_untagged_registry_and_manual_channels_after_tag_removal() {
let directory = TestDirectory::new();