fix: accept existing handles with trailing underscores
This commit is contained in:
@@ -143,6 +143,8 @@ Press **`U` (Shift-U)** on a video to browse its creator directly, including cha
|
||||
|
||||
When search results omit a channel handle, `s` reuses its saved mapping or looks up the handle from the channel's canonical ID before opening the editor. Esc cancels a pending lookup without saving. Failed lookups or mismatched channel metadata leave the config unchanged.
|
||||
|
||||
Tubular validates handles as channel addresses, not against [YouTube's current handle-creation guidelines](https://support.google.com/youtube/answer/11585688). Existing handles such as `@hontame_` keep their leading/trailing separators and round-trip through saved mappings and categories. Whitespace inside handles, control characters, slashes, query strings, and fragments remain rejected.
|
||||
|
||||
- **Normal:** `j` / `k` or arrow keys navigate; Space toggles a tag; `i` edits the filter; `R` renames the selected tag.
|
||||
- **Insert:** type a case-insensitive substring to filter live; arrow keys or Ctrl-J / Ctrl-K navigate matches. Enter or Esc returns to normal mode without saving or clearing the filter.
|
||||
- Filtering only hides rows: checked and unchecked staged memberships outside the filter are retained. With no matches, navigation and toggling do nothing. Reopening the dialog clears its filter.
|
||||
|
||||
+29
-2
@@ -563,14 +563,13 @@ pub(crate) fn normalize_channel(input: &str) -> io::Result<String> {
|
||||
.or_else(|| input.strip_prefix("https://youtube.com/"));
|
||||
let address = url_address.unwrap_or(input);
|
||||
if let Some(handle) = address.strip_prefix('@') {
|
||||
// Validate address safety, not creation rules: existing handles can have edge separators.
|
||||
let length = handle.chars().count();
|
||||
if length == 0
|
||||
|| length > 30
|
||||
|| !handle
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'))
|
||||
|| !handle.starts_with(char::is_alphanumeric)
|
||||
|| !handle.ends_with(char::is_alphanumeric)
|
||||
{
|
||||
return Err(invalid("Invalid YouTube channel handle"));
|
||||
}
|
||||
@@ -787,6 +786,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_edge_separator_handles_can_be_saved_and_reloaded() {
|
||||
let channel = "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A";
|
||||
for handle in ["@hontame_", "@_creator", "@.creator-"] {
|
||||
let directory = TestDirectory::new();
|
||||
let path = directory.0.join("config.toml");
|
||||
let tags = BTreeMap::from([("Immersion".to_owned(), true)]);
|
||||
let saved = set_channel_tags(&path, &tags, handle, channel).unwrap();
|
||||
assert_eq!(saved.channels[handle], channel);
|
||||
assert_eq!(saved.categories["Immersion"], [channel]);
|
||||
let reloaded = load(&path).unwrap();
|
||||
assert_eq!(reloaded.channels, saved.channels);
|
||||
assert_eq!(reloaded.categories, saved.categories);
|
||||
let unchecked = BTreeMap::from([("Immersion".to_owned(), false)]);
|
||||
set_channel_tags(
|
||||
&path,
|
||||
&unchecked,
|
||||
&format!("https://www.youtube.com/{handle}"),
|
||||
channel,
|
||||
)
|
||||
.unwrap();
|
||||
let reloaded = load(&path).unwrap();
|
||||
assert!(reloaded.categories["Immersion"].is_empty());
|
||||
assert_eq!(reloaded.channels[handle], channel);
|
||||
assert_eq!(reloaded.channels.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_remaps_survive_all_saves_and_section_orders() {
|
||||
use crate::keys::{Action, Context};
|
||||
|
||||
+1
-1
@@ -877,7 +877,7 @@ mod tests {
|
||||
#[test]
|
||||
fn channel_handles_require_the_requested_identity_and_a_valid_handle() {
|
||||
let id = "UChAQQ9at2MDlXjvQX3vL3Yw";
|
||||
for handle in ["@ch.1676", "@敷嶋てとら"] {
|
||||
for handle in ["@ch.1676", "@敷嶋てとら", "@hontame_"] {
|
||||
let metadata = serde_json::json!({"channel_id": id, "uploader_id": handle}).to_string();
|
||||
assert_eq!(channel_handle(&metadata, id).unwrap(), handle);
|
||||
assert!(channel_handle(&metadata, "UCaYhcUwRBNscFNUKTjgPFiA").is_err());
|
||||
|
||||
Reference in New Issue
Block a user