feat: combine subscriptions in a general Feed view
This commit is contained in:
+77
-12
@@ -11,9 +11,23 @@ use toml_edit::{Array, DocumentMut, Item, Table, value};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Config {
|
||||
pub channels: BTreeMap<String, String>,
|
||||
pub categories: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn subscriptions(&self) -> Vec<String> {
|
||||
let mut channels: Vec<_> = self
|
||||
.channels
|
||||
.values()
|
||||
.chain(self.categories.values().flatten())
|
||||
.collect();
|
||||
channels.sort_unstable();
|
||||
channels.dedup();
|
||||
channels.into_iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_path() -> io::Result<PathBuf> {
|
||||
resolve_path(
|
||||
std::env::var_os("XDG_CONFIG_HOME"),
|
||||
@@ -35,7 +49,12 @@ fn resolve_path(xdg: Option<OsString>, home: Option<OsString>) -> io::Result<Pat
|
||||
|
||||
pub fn load(path: &Path) -> io::Result<Config> {
|
||||
let (_, document) = read_document(path)?;
|
||||
parse_categories(&document, &parse_channels(&document)?)
|
||||
let channels = parse_channels(&document)?;
|
||||
let categories = parse_categories(&document, &channels)?;
|
||||
Ok(Config {
|
||||
channels,
|
||||
categories,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_channel_tags(
|
||||
@@ -163,9 +182,12 @@ pub fn set_channel_tags(
|
||||
}
|
||||
}
|
||||
}
|
||||
let config = parse_categories(&document, &mappings)?;
|
||||
let categories = parse_categories(&document, &mappings)?;
|
||||
save(path, original.as_deref(), document.to_string().as_bytes())?;
|
||||
Ok(config)
|
||||
Ok(Config {
|
||||
channels: mappings,
|
||||
categories,
|
||||
})
|
||||
}
|
||||
|
||||
fn invalid(message: impl Into<String>) -> io::Error {
|
||||
@@ -296,10 +318,10 @@ fn read_document(path: &Path) -> io::Result<(Option<Vec<u8>>, DocumentMut)> {
|
||||
fn parse_categories(
|
||||
document: &DocumentMut,
|
||||
mappings: &BTreeMap<String, String>,
|
||||
) -> io::Result<Config> {
|
||||
let mut config = Config::default();
|
||||
) -> io::Result<BTreeMap<String, Vec<String>>> {
|
||||
let mut parsed = BTreeMap::new();
|
||||
let Some(item) = document.get("categories") else {
|
||||
return Ok(config);
|
||||
return Ok(parsed);
|
||||
};
|
||||
let categories = item
|
||||
.as_table_like()
|
||||
@@ -321,17 +343,13 @@ fn parse_categories(
|
||||
normalized.push(channel);
|
||||
}
|
||||
}
|
||||
if config
|
||||
.categories
|
||||
.insert(name.to_owned(), normalized)
|
||||
.is_some()
|
||||
{
|
||||
if parsed.insert(name.to_owned(), normalized).is_some() {
|
||||
return Err(invalid(format!(
|
||||
"Duplicate category name after trimming: {name:?}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -430,6 +448,10 @@ mod tests {
|
||||
assert!(!path.parent().unwrap().exists());
|
||||
let tags = BTreeMap::from([(" Music ".to_owned(), true)]);
|
||||
let config = set_channel_tags(&path, &tags, "@example", id).unwrap();
|
||||
assert_eq!(
|
||||
config.channels["@example"],
|
||||
format!("https://www.youtube.com/channel/{id}")
|
||||
);
|
||||
assert_eq!(
|
||||
config.categories["Music"],
|
||||
[format!("https://www.youtube.com/channel/{id}")]
|
||||
@@ -625,6 +647,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscriptions_include_untagged_registry_and_manual_channels_after_tag_removal() {
|
||||
let directory = TestDirectory::new();
|
||||
let path = directory.0.join("config.toml");
|
||||
let id = "UC4QobU6STFB0P71PMvOGN5A";
|
||||
let url = format!("https://www.youtube.com/channel/{id}");
|
||||
let other_id = "UCabcdefghijklmnopqrstuv";
|
||||
let other_url = format!("https://www.youtube.com/channel/{other_id}");
|
||||
let registry =
|
||||
format!("[channels]\n'https://youtube.com/@old' = '{id}'\n'@alias' = '{url}'\n");
|
||||
fs::write(&path, ®istry).unwrap();
|
||||
let config = load(&path).unwrap();
|
||||
assert!(config.categories.is_empty());
|
||||
assert_eq!(config.channels["@old"], url);
|
||||
assert_eq!(config.subscriptions(), std::slice::from_ref(&url));
|
||||
|
||||
let config = set_channel_tags(&path, &BTreeMap::new(), "@untagged", other_id).unwrap();
|
||||
assert!(config.categories.is_empty());
|
||||
assert_eq!(config.subscriptions(), [url.clone(), other_url.clone()]);
|
||||
assert_eq!(load(&path).unwrap().subscriptions(), config.subscriptions());
|
||||
|
||||
fs::write(
|
||||
&path,
|
||||
format!(
|
||||
"{registry}[categories]\nTagged = ['@old', '@alias', '{url}', '{id}']\nManual = ['@RustVideos', 'https://youtube.com/@RustVideos', '{other_id}', '{other_url}']\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let expected = vec![
|
||||
"https://www.youtube.com/@RustVideos".to_owned(),
|
||||
url.clone(),
|
||||
other_url,
|
||||
];
|
||||
let config = load(&path).unwrap();
|
||||
assert_eq!(config.categories["Tagged"], std::slice::from_ref(&url));
|
||||
assert_eq!(config.subscriptions(), expected);
|
||||
let tags = BTreeMap::from([("Tagged".to_owned(), false)]);
|
||||
let config = set_channel_tags(&path, &tags, "@current", id).unwrap();
|
||||
assert!(config.categories["Tagged"].is_empty());
|
||||
assert_eq!(config.subscriptions(), expected);
|
||||
assert_eq!(load(&path).unwrap().subscriptions(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paths_and_channel_addresses_are_strict() {
|
||||
let home = Some(OsString::from("/home/test"));
|
||||
|
||||
Reference in New Issue
Block a user