#![allow(clippy::use_self)]
pub mod builders;
pub mod id;
mod serializers;
use std::{
fmt::{self, Display},
ops::Deref,
string::ToString,
};
use chrono::{DateTime, Duration, Utc};
use serde::{self, Deserialize, Serialize};
use serde_with::{CommaSeparator, DisplayFromStr, DurationSeconds};
use crate::util::is_default;
use self::id::{ChannelId, VideoId};
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct VideoFilter {
pub channel_id: Option<ChannelId>,
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub id: Vec<VideoId>,
pub org: Option<Organisation>,
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<ExtraVideoInfo>,
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub lang: Vec<Language>,
pub max_upcoming_hours: u32,
pub mentioned_channel_id: Option<ChannelId>,
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub status: Vec<VideoStatus>,
pub topic: Option<String>,
#[serde(rename = "type")]
pub video_type: VideoType,
#[serde(with = "serde_with::rust::display_fromstr")]
#[serde(skip_serializing_if = "is_default")]
pub paginated: bool,
pub limit: u32,
pub offset: i32,
#[serde(rename = "sort")]
pub sort_by: VideoSortingCriteria,
pub order: Order,
}
impl VideoFilter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for VideoFilter {
fn default() -> Self {
Self {
channel_id: None,
id: Vec::new(),
include: vec![ExtraVideoInfo::LiveInfo],
lang: vec![Language::All],
limit: 100,
max_upcoming_hours: 48,
mentioned_channel_id: None,
offset: 0,
order: Order::Descending,
org: Some(Organisation::Hololive),
paginated: true,
sort_by: VideoSortingCriteria::AvailableAt,
status: Vec::new(),
topic: None,
video_type: VideoType::Stream,
}
}
}
impl Display for VideoFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {{ channel_id: {}, id: {}, org: {}, include: {}, lang: {}, max_upcoming_hours: {}, mentioned_channel_id: {}, paginated: {}, limit: {}, offset: {}, sort_by: {}, order: {}, status: {}, topic: {}, video_type: {} }}",
stringify!(VideoFilter),
self.channel_id.as_ref().map_or("None", |id| &*id.0),
self.id.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "),
self.org.as_ref().map_or("None".to_owned(), ToString::to_string),
self.include.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "),
self.lang.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "),
self.max_upcoming_hours,
self.mentioned_channel_id.as_ref().map_or("None", |id| &*id.0),
self.paginated,
self.limit,
self.offset,
self.sort_by,
self.order,
self.status.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "),
self.topic.as_ref().map_or("None".to_owned(), ToString::to_string),
self.video_type,
)
}
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ChannelVideoFilter {
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<ExtraVideoInfo>,
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
#[serde(rename = "lang")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<Language>,
#[serde(with = "serde_with::rust::display_fromstr")]
#[serde(skip_serializing_if = "is_default")]
pub paginated: bool,
pub limit: u32,
pub offset: i32,
}
impl ChannelVideoFilter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for ChannelVideoFilter {
fn default() -> Self {
Self {
include: vec![ExtraVideoInfo::LiveInfo],
languages: vec![Language::All],
limit: 100,
offset: 0,
paginated: true,
}
}
}
impl Display for ChannelVideoFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {{ include: {}, lang: {}, paginated: {}, limit: {}, offset: {} }}",
stringify!(ChannelVideoFilter),
self.include
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(", "),
self.languages
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(", "),
self.paginated,
self.limit,
self.offset
)
}
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ChannelFilter {
#[serde(rename = "lang")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<Language>,
pub order: Order,
#[serde(rename = "sort")]
pub sort_by: ChannelSortingCriteria,
#[serde(rename = "org")]
pub organisation: Option<Organisation>,
#[serde(rename = "type")]
pub channel_type: Option<ChannelType>,
pub limit: u32,
pub offset: i32,
}
impl Default for ChannelFilter {
fn default() -> Self {
Self {
languages: Vec::new(),
order: Order::Ascending,
sort_by: ChannelSortingCriteria::Organisation,
organisation: None,
channel_type: None,
limit: 25,
offset: 0,
}
}
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct VideoSearch {
#[serde(rename = "sort")]
pub sort_order: SearchOrder,
#[serde(rename = "lang")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<Language>,
#[serde(rename = "target")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub types: Vec<VideoType>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub conditions: Vec<VideoSearchCondition>,
#[serde(rename = "topic")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub topics: Vec<String>,
#[serde(rename = "vch")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<ChannelId>,
#[serde(rename = "org")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub organisations: Vec<Organisation>,
#[serde(with = "serde_with::rust::display_fromstr")]
#[serde(skip_serializing_if = "is_default")]
pub paginated: bool,
pub limit: u32,
pub offset: i32,
}
impl Default for VideoSearch {
fn default() -> Self {
Self {
sort_order: SearchOrder::Newest,
languages: Vec::default(),
types: Vec::default(),
conditions: Vec::default(),
topics: Vec::default(),
channels: Vec::default(),
organisations: Vec::default(),
paginated: true,
limit: 30,
offset: 0,
}
}
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[serde(rename_all(serialize = "snake_case"))]
pub enum VideoSearchCondition {
Text(String),
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct CommentSearch {
pub search: String,
#[serde(rename = "sort")]
pub sort_order: SearchOrder,
#[serde(rename = "lang")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<Language>,
#[serde(rename = "target")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub types: Vec<VideoType>,
#[serde(rename = "topic")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub topics: Vec<String>,
#[serde(rename = "vch")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<ChannelId>,
#[serde(rename = "org")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub organisations: Vec<Organisation>,
#[serde(with = "serde_with::rust::display_fromstr")]
#[serde(skip_serializing_if = "is_default")]
pub paginated: bool,
pub limit: u32,
pub offset: i32,
}
impl Default for CommentSearch {
fn default() -> Self {
Self {
search: String::default(),
sort_order: SearchOrder::Newest,
languages: Vec::default(),
types: Vec::default(),
topics: Vec::default(),
channels: Vec::default(),
organisations: Vec::default(),
paginated: true,
limit: 30,
offset: 0,
}
}
}
#[derive(Serialize, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[serde(rename_all(serialize = "snake_case"))]
pub enum SearchOrder {
Oldest,
Newest,
}
#[non_exhaustive]
#[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all(serialize = "snake_case"))]
pub enum ExtraVideoInfo {
Clips,
Refers,
Sources,
Simulcasts,
Mentions,
Description,
LiveInfo,
ChannelStats,
Songs,
}
impl Display for ExtraVideoInfo {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
ExtraVideoInfo::Clips => f.pad("clips"),
ExtraVideoInfo::Refers => f.pad("refers"),
ExtraVideoInfo::Sources => f.pad("sources"),
ExtraVideoInfo::Simulcasts => f.pad("simulcasts"),
ExtraVideoInfo::Mentions => f.pad("mentions"),
ExtraVideoInfo::Description => f.pad("description"),
ExtraVideoInfo::LiveInfo => f.pad("live_info"),
ExtraVideoInfo::ChannelStats => f.pad("channel_stats"),
ExtraVideoInfo::Songs => f.pad("songs"),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Language {
All,
English,
Spanish,
Indonesian,
Japanese,
Korean,
Russian,
Chinese,
Other(String),
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Order {
#[serde(rename = "asc")]
Ascending,
#[serde(rename = "desc")]
Descending,
}
impl Display for Order {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Order::Ascending => f.pad("Ascending"),
Order::Descending => f.pad("Descending"),
}
}
}
#[non_exhaustive]
#[allow(clippy::use_self)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Organisation {
Hololive,
Nijisanji,
Independents,
Other(String),
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum VideoSortingCriteria {
Id,
Title,
Type,
#[serde(rename = "topic_id")]
Topics,
PublishedAt,
AvailableAt,
Duration,
Status,
StartScheduled,
StartActual,
EndActual,
LiveViewers,
Description,
#[serde(rename = "songcount")]
SongCount,
ChannelId,
}
impl Display for VideoSortingCriteria {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
VideoSortingCriteria::Id => f.pad("Id"),
VideoSortingCriteria::Title => f.pad("Title"),
VideoSortingCriteria::Type => f.pad("Type"),
VideoSortingCriteria::Topics => f.pad("Topics"),
VideoSortingCriteria::PublishedAt => f.pad("PublishedAt"),
VideoSortingCriteria::AvailableAt => f.pad("AvailableAt"),
VideoSortingCriteria::Duration => f.pad("Duration"),
VideoSortingCriteria::Status => f.pad("Status"),
VideoSortingCriteria::StartScheduled => f.pad("StartScheduled"),
VideoSortingCriteria::StartActual => f.pad("StartActual"),
VideoSortingCriteria::EndActual => f.pad("EndActual"),
VideoSortingCriteria::LiveViewers => f.pad("LiveViewers"),
VideoSortingCriteria::Description => f.pad("Description"),
VideoSortingCriteria::SongCount => f.pad("SongCount"),
VideoSortingCriteria::ChannelId => f.pad("ChannelId"),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ChannelSortingCriteria {
Id,
Name,
EnglishName,
Type,
#[serde(rename = "org")]
Organisation,
#[serde(rename = "suborg")]
SubOrganisation,
Photo,
Banner,
Twitter,
VideoCount,
SubscriberCount,
ViewCount,
ClipCount,
#[serde(rename = "lang")]
Language,
PublishedAt,
Inactive,
Description,
}
impl Display for ChannelSortingCriteria {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
ChannelSortingCriteria::Id => f.pad("Id"),
ChannelSortingCriteria::Name => f.pad("Name"),
ChannelSortingCriteria::EnglishName => f.pad("EnglishName"),
ChannelSortingCriteria::Type => f.pad("Type"),
ChannelSortingCriteria::Organisation => f.pad("Organisation"),
ChannelSortingCriteria::SubOrganisation => f.pad("SubOrganisation"),
ChannelSortingCriteria::Photo => f.pad("Photo"),
ChannelSortingCriteria::Banner => f.pad("Banner"),
ChannelSortingCriteria::Twitter => f.pad("Twitter"),
ChannelSortingCriteria::VideoCount => f.pad("VideoCount"),
ChannelSortingCriteria::SubscriberCount => f.pad("SubscriberCount"),
ChannelSortingCriteria::ViewCount => f.pad("ViewCount"),
ChannelSortingCriteria::ClipCount => f.pad("ClipCount"),
ChannelSortingCriteria::Language => f.pad("Language"),
ChannelSortingCriteria::PublishedAt => f.pad("PublishedAt"),
ChannelSortingCriteria::Inactive => f.pad("Inactive"),
ChannelSortingCriteria::Description => f.pad("Description"),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum VideoType {
Stream,
Clip,
}
impl Display for VideoType {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
VideoType::Stream => f.pad("Stream"),
VideoType::Clip => f.pad("Clip"),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ChannelVideoType {
Clips,
Videos,
Collabs,
}
impl Display for ChannelVideoType {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
ChannelVideoType::Clips => f.pad("Clips"),
ChannelVideoType::Videos => f.pad("Videos"),
ChannelVideoType::Collabs => f.pad("Collabs"),
}
}
}
#[non_exhaustive]
#[allow(dead_code)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum VideoStatus {
New,
Upcoming,
Live,
Past,
Missing,
}
impl Display for VideoStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
VideoStatus::New => f.pad("new"),
VideoStatus::Upcoming => f.pad("upcoming"),
VideoStatus::Live => f.pad("live"),
VideoStatus::Past => f.pad("past"),
VideoStatus::Missing => f.pad("missing"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum PaginatedTotal {
U32(u32),
String(#[serde(with = "serde_with::rust::display_fromstr")] u32),
}
impl From<PaginatedTotal> for u32 {
#[inline]
fn from(total: PaginatedTotal) -> Self {
match total {
PaginatedTotal::U32(n) | PaginatedTotal::String(n) => n,
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum PaginatedResult<T> {
Items(#[serde(default = "Default::default")] Vec<T>),
Page {
total: PaginatedTotal,
#[serde(default = "Default::default")]
items: Vec<T>,
},
}
impl<T> PaginatedResult<T> {
#[must_use]
#[inline]
pub fn items(&self) -> &[T] {
match self {
PaginatedResult::Items(items) | PaginatedResult::Page { items, .. } => items,
}
}
#[must_use]
#[inline]
#[allow(clippy::missing_const_for_fn)]
pub fn into_items(self) -> Vec<T> {
match self {
PaginatedResult::Items(items) | PaginatedResult::Page { items, .. } => items,
}
}
}
impl<T> Deref for PaginatedResult<T> {
type Target = [T];
#[inline]
fn deref(&self) -> &Self::Target {
self.items()
}
}
impl<T> IntoIterator for PaginatedResult<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<Self::Item>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
match self {
PaginatedResult::Items(items) | PaginatedResult::Page { items, .. } => {
items.into_iter()
}
}
}
}
impl<T> From<PaginatedResult<T>> for Vec<T> {
#[inline]
fn from(result: PaginatedResult<T>) -> Self {
result.into_items()
}
}
#[derive(Deserialize, Debug, Clone, Eq, PartialOrd, Ord)]
pub struct Video {
pub id: VideoId,
pub title: String,
#[serde(rename = "type")]
pub video_type: VideoType,
#[serde(default)]
#[serde(rename = "topic_id")]
pub topic: Option<String>,
#[serde(default)]
pub published_at: Option<DateTime<Utc>>,
pub available_at: DateTime<Utc>,
#[serde(with = "serde_with::As::<Option<DurationSeconds<i64>>>")]
#[serde(default)]
pub duration: Option<Duration>,
pub status: VideoStatus,
#[serde(flatten)]
pub live_info: VideoLiveInfo,
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "songcount")]
#[serde(default)]
pub song_count: Option<u32>,
#[serde(alias = "channel_id")]
pub channel: VideoChannel,
}
impl PartialEq for Video {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.title == other.title
&& self.video_type == other.video_type
&& self.topic == other.topic
&& self.published_at == other.published_at
&& self.available_at == other.available_at
&& self.status == other.status
&& self.live_info == other.live_info
&& self.description == other.description
&& self.song_count == other.song_count
&& self.channel == other.channel
}
}
impl std::hash::Hash for Video {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.title.hash(state);
self.video_type.hash(state);
self.topic.hash(state);
self.published_at.hash(state);
self.available_at.hash(state);
self.status.hash(state);
self.live_info.hash(state);
self.description.hash(state);
self.song_count.hash(state);
self.channel.hash(state);
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ChannelMin {
pub id: ChannelId,
pub name: String,
#[serde(default)]
pub english_name: Option<String>,
#[serde(rename = "type")]
pub channel_type: Option<ChannelType>,
pub photo: String,
#[serde(default)]
pub org: Option<Organisation>,
#[serde(flatten)]
pub stats: ChannelStats,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
#[serde(default)]
pub inactive: bool,
#[serde(rename = "type")]
pub channel_type: ChannelType,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub lang: Option<Language>,
#[serde(default)]
pub english_name: Option<String>,
#[serde(default)]
pub org: Option<Organisation>,
#[serde(default)]
pub suborg: Option<String>,
#[serde(default)]
pub photo: Option<String>,
#[serde(default)]
pub banner: Option<String>,
#[serde(default)]
pub twitter: Option<String>,
#[serde(flatten)]
pub stats: ChannelStats,
#[serde(default)]
pub top_topics: Vec<String>,
pub published_at: Option<DateTime<Utc>>,
pub crawled_at: Option<DateTime<Utc>>,
pub comments_crawled_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ChannelStats {
#[serde(with = "serde_with::As::<Option<DisplayFromStr>>")]
#[serde(default)]
pub video_count: Option<u32>,
#[serde(with = "serde_with::As::<Option<DisplayFromStr>>")]
#[serde(default)]
pub subscriber_count: Option<u32>,
#[serde(with = "serde_with::As::<Option<DisplayFromStr>>")]
#[serde(default)]
pub view_count: Option<u32>,
#[serde(default)]
pub clip_count: Option<u32>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum VideoChannel {
Id(ChannelId),
Min(ChannelMin),
}
impl VideoChannel {
#[inline]
#[must_use]
pub const fn id(&self) -> &ChannelId {
match self {
Self::Id(id) => id,
Self::Min(d) => &d.id,
}
}
}
#[non_exhaustive]
#[allow(dead_code)]
#[derive(Deserialize, Serialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ChannelType {
VTuber,
Subber,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VideoFull {
#[serde(flatten)]
pub video: Video,
#[serde(default)]
pub clips: Vec<Video>,
#[serde(default)]
pub sources: Vec<Video>,
#[serde(default)]
pub refers: Vec<Video>,
#[serde(default)]
pub simulcasts: Vec<Video>,
#[serde(default)]
pub mentions: Vec<ChannelMin>,
#[serde(default)]
#[serde(rename = "songcount")]
pub song_count: Option<u32>,
#[serde(default)]
pub songs: Vec<Song>,
#[serde(default)]
pub comments: Vec<Comment>,
#[serde(default)]
#[serde(alias = "recommendations")]
pub related: Vec<Video>,
}
#[derive(
Deserialize, Serialize, Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[serde(default)]
pub struct VideoLiveInfo {
pub start_scheduled: Option<DateTime<Utc>>,
pub start_actual: Option<DateTime<Utc>>,
pub end_actual: Option<DateTime<Utc>>,
pub live_viewers: Option<u32>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Comment {
pub comment_key: String,
#[serde(default)]
pub video_id: Option<VideoId>,
pub message: String,
}
impl Display for Comment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialOrd, Ord)]
pub struct Song {
pub name: String,
#[serde(rename = "original_artist")]
pub artist: String,
#[serde(rename = "art")]
pub artwork: Option<String>,
#[serde(rename = "itunesid")]
pub itunes_id: Option<u64>,
#[serde(with = "serde_with::As::<DurationSeconds<i64>>")]
pub start: Duration,
#[serde(with = "serde_with::As::<DurationSeconds<i64>>")]
pub end: Duration,
}
impl PartialEq for Song {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.artist == other.artist
&& self.artwork == other.artwork
&& self.itunes_id == other.itunes_id
}
}
impl std::hash::Hash for Song {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.artist.hash(state);
self.artwork.hash(state);
self.itunes_id.hash(state);
}
}
impl Display for Song {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} by {}", self.name, self.artist)
}
}