1use alloc::borrow::{Cow, ToOwned};
2use alloc::string::String;
3
4#[cfg(feature = "regex")]
5use regex::Regex;
6use serde::{
7 de::{Error as SerdeError, Unexpected},
8 Deserialize, Deserializer, Serialize,
9};
10use url::Url;
11
12use core::cmp::Ordering;
13
14#[cfg(feature = "std")]
15use crate::{Error, Result};
16
17pub type IdBuf = Id<'static>;
19
20#[cfg(feature = "regex")]
27pub static ID_PATTERNS: [&once_cell::sync::Lazy<Regex>; 5] = [
28 &WATCH_URL_PATTERN,
29 &SHORTS_URL_PATTERN,
30 &EMBED_URL_PATTERN,
31 &SHARE_URL_PATTERN,
32 &ID_PATTERN
33];
34#[cfg(feature = "regex")]
36pub static WATCH_URL_PATTERN: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(||
37 Regex::new(r"^(https?://)?(www\.)?youtube.\w\w\w?/watch\?v=(?P<id>[a-zA-Z0-9_-]{11})(&.*)?$").unwrap()
39);
40#[cfg(feature = "regex")]
42pub static SHORTS_URL_PATTERN: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(||
43 Regex::new(r"^(https?://)?(www\.)?youtube.\w\w\w?/shorts/(?P<id>[a-zA-Z0-9_-]{11})(\?.*)?$").unwrap()
44);
45#[cfg(feature = "regex")]
47pub static EMBED_URL_PATTERN: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(||
48 Regex::new(r"^(https?://)?(www\.)?youtube.\w\w\w?/embed/(?P<id>[a-zA-Z0-9_-]{11})\\?(\?.*)?$").unwrap()
50);
51#[cfg(feature = "regex")]
53pub static SHARE_URL_PATTERN: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(||
54 Regex::new(r"^(https?://)?youtu\.be/(?P<id>[a-zA-Z0-9_-]{11})$").unwrap()
56);
57#[cfg(feature = "regex")]
59pub static ID_PATTERN: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(||
60 Regex::new("^(?P<id>[a-zA-Z0-9_-]{11})$").unwrap()
62);
63
64#[derive(Clone, Debug, Serialize, Hash)]
88pub struct Id<'a>(Cow<'a, str>);
89
90#[allow(clippy::should_implement_trait)]
91impl<'a> Id<'a> {
92 cfg_if::cfg_if! {
93 if #[cfg(feature = "regex")] {
94 pub fn from_raw(raw: &'a str) -> Result<Self> {
95 ID_PATTERNS
96 .iter()
97 .find_map(|pattern|
98 pattern
99 .captures(raw)
100 .map(|c| {
101 let id = c.name("id").unwrap().as_str();
103 Self(Cow::Borrowed(id))
104 })
105 )
106 .ok_or(Error::BadIdFormat)
107 }
108
109 #[inline]
110 pub fn from_str(id: &'a str) -> Result<Self> {
111 match ID_PATTERN.is_match(id) {
112 true => Ok(Self(Cow::Borrowed(id))),
113 false => Err(Error::BadIdFormat)
114 }
115 }
116 } else {
117 #[inline]
118 pub fn from_str(id: &'a str) -> Option<Self> {
119 match Self::check_str(id) {
120 Ok(_) => Some(Self(Cow::Borrowed(id))),
121 Err(_) => None
122 }
123 }
124
125 #[inline]
126 fn check_str(id: &'_ str) -> Result<(), ()> {
127 if id.len() != 11 {
128 return Err(());
129 }
130
131 let only_allowed_chars = id
132 .chars()
133 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
134
135 if only_allowed_chars {
136 Ok(())
137 } else {
138 Err(())
139 }
140 }
141 }
142 }
143}
144
145impl<'a> Id<'a> {
146 #[inline]
147 pub fn is_borrowed(&self) -> bool {
148 matches!(self.0, Cow::Borrowed(_))
149 }
150
151 #[inline]
152 pub fn is_owned(&self) -> bool {
153 matches!(self.0, Cow::Owned(_))
154 }
155
156 #[inline]
157 pub fn make_owned(&mut self) -> &mut Self {
158 if let Cow::Borrowed(id) = self.0 {
159 self.0 = Cow::Owned(id.to_owned());
160 }
161 self
162 }
163
164 #[inline]
165 #[must_use]
166 pub fn into_owned(self) -> IdBuf {
167 match self.0 {
168 Cow::Owned(id) => Id(Cow::Owned(id)),
169 Cow::Borrowed(id) => Id(Cow::Owned(id.to_owned()))
170 }
171 }
172
173 #[inline]
174 #[must_use]
175 pub fn as_owned(&self) -> IdBuf {
176 self
177 .clone()
178 .into_owned()
179 }
180
181 #[inline]
182 #[must_use]
183 pub fn as_borrowed(&'a self) -> Self {
184 Self(Cow::Borrowed(&self.0))
185 }
186
187 #[inline]
188 #[must_use]
189 pub fn as_str(&self) -> &str {
190 self.0.as_ref()
191 }
192
193 #[inline]
194 #[must_use]
195 pub fn watch_url(&self) -> Url {
196 Url::parse_with_params(
197 "https://www.youtube.com/watch?",
198 &[("v", self.as_str())],
199 ).unwrap()
200 }
201
202 #[inline]
203 #[must_use]
204 pub fn shorts_url(&self) -> Url {
205 let mut url = Url::parse("https://www.youtube.com/shorts")
206 .unwrap();
207 url
208 .path_segments_mut()
209 .unwrap()
210 .push(self.as_str());
211 url
212 }
213
214 #[inline]
215 #[must_use]
216 pub fn embed_url(&self) -> Url {
217 let mut url = Url::parse("https://www.youtube.com/embed")
218 .unwrap();
219 url
220 .path_segments_mut()
221 .unwrap()
222 .push(self.as_str());
223 url
224 }
225
226 #[inline]
227 #[must_use]
228 pub fn share_url(&self) -> Url {
229 let mut url = Url::parse("https://youtu.be")
230 .unwrap();
231 url
232 .path_segments_mut()
233 .unwrap()
234 .push(self.as_str());
235 url
236 }
237}
238
239impl IdBuf {
240 cfg_if::cfg_if! {
241 if #[cfg(feature = "regex")] {
242 #[inline]
243 pub fn from_string(id: String) -> Result<Self, String> {
244 match ID_PATTERN.is_match(id.as_str()) {
245 true => Ok(Self(Cow::Owned(id))),
246 false => Err(id)
247 }
248 }
249 } else {
250 #[inline]
251 pub fn from_string(id: String) -> Result<Self, String> {
252 match Self::check_str(&id) {
253 Ok(_) => Ok(Self(Cow::Owned(id))),
254 Err(_) => Err(id)
255 }
256 }
257 }
258 }
259}
260
261impl<'de> Id<'de> {
262 #[inline]
263 pub fn deserialize_borrowed<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
264 where
265 D: Deserializer<'de> {
266 let raw = <&'de str>::deserialize(deserializer)?;
267 #[cfg(not(all(feature = "regex", feature = "std")))]
268 let res = Self::from_str(raw).ok_or(());
269 #[cfg(all(feature = "regex", feature = "std"))]
270 let res = Self::from_raw(raw);
271
272 res
273 .map_err(|_| D::Error::invalid_value(
274 Unexpected::Str(raw),
275 &"expected a valid youtube video identifier",
276 ))
277 }
278}
279
280impl<'de> Deserialize<'de> for Id<'static> {
281 #[inline]
282 fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
283 where
284 D: Deserializer<'de> {
285 let raw = String::deserialize(deserializer)?;
286 Self::from_string(raw)
287 .map_err(|s| D::Error::invalid_value(
288 Unexpected::Str(&s),
289 &"expected a valid youtube video identifier",
290 ))
291 }
292}
293
294
295impl core::fmt::Display for Id<'_> {
296 #[inline]
297 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298 f.write_str(self.as_str())
299 }
300}
301
302impl core::ops::Deref for Id<'_> {
303 type Target = str;
304
305 #[inline]
306 fn deref(&self) -> &Self::Target {
307 self.as_str()
308 }
309}
310
311impl core::convert::AsRef<str> for Id<'_> {
312 #[inline]
313 fn as_ref(&self) -> &str {
314 self.as_str()
315 }
316}
317
318impl<T> core::cmp::PartialEq<T> for Id<'_>
319 where
320 T: core::convert::AsRef<str> {
321 #[inline]
322 fn eq(&self, other: &T) -> bool {
323 core::cmp::PartialEq::eq(
324 self.as_str(),
325 other.as_ref(),
326 )
327 }
328}
329
330impl core::cmp::Eq for Id<'_> {}
331
332impl core::cmp::Ord for Id<'_> {
333 #[inline]
334 fn cmp(&self, other: &Self) -> Ordering {
335 self.as_str().cmp(other.as_str())
336 }
337}
338
339impl<T> core::cmp::PartialOrd<T> for Id<'_>
340 where
341 T: AsRef<str> {
342 #[inline]
343 fn partial_cmp(&self, other: &T) -> Option<Ordering> {
344 core::cmp::PartialOrd::partial_cmp(
345 self.as_str(),
346 other.as_ref(),
347 )
348 }
349}