Compare commits
4 commits
86a4bd9762
...
7fe08e37ec
Author | SHA1 | Date | |
---|---|---|---|
7fe08e37ec | |||
585faffd32 | |||
a451a5dbe9 | |||
9e4a5de891 |
25 changed files with 995 additions and 633 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2887,6 +2887,7 @@ dependencies = [
|
||||||
"path_macro",
|
"path_macro",
|
||||||
"serde",
|
"serde",
|
||||||
"smart-default",
|
"smart-default",
|
||||||
|
"time",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "insert into track_aliases (src_id, service, track_id) values ($1, $2, $3)\non conflict (src_id, service) do update set track_id=excluded.track_id",
|
"query": "insert into track_aliases (src_id, service, track_id, match_data) values ($1, $2, $3, $4)\non conflict (src_id, service) do update set\ntrack_id = coalesce(excluded.track_id, track_aliases.track_id),\nmatch_data = coalesce(excluded.match_data, track_aliases.match_data)",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -19,10 +19,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Int4"
|
"Int4",
|
||||||
|
"Jsonb"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "54911be75e68b9ddda44352f492fb7f500e86ef25c12a6840ea9e61b219f0d7c"
|
"hash": "2431c70614cb2e2f51a6e5dcf1798b3346c088eb8aab95a658e6455476347f3c"
|
||||||
}
|
}
|
|
@ -203,7 +203,8 @@ ADD CONSTRAINT artist_aliases_artists_fk FOREIGN KEY (artist_id) REFERENCES arti
|
||||||
CREATE TABLE track_aliases (
|
CREATE TABLE track_aliases (
|
||||||
src_id text NOT NULL,
|
src_id text NOT NULL,
|
||||||
service music_service NOT NULL,
|
service music_service NOT NULL,
|
||||||
track_id integer NOT NULL,
|
track_id integer,
|
||||||
|
match_data jsonb,
|
||||||
CONSTRAINT track_aliases_pk PRIMARY KEY (src_id, service)
|
CONSTRAINT track_aliases_pk PRIMARY KEY (src_id, service)
|
||||||
);
|
);
|
||||||
COMMENT ON TABLE track_aliases IS E'Track IDs from secondary sources';
|
COMMENT ON TABLE track_aliases IS E'Track IDs from secondary sources';
|
||||||
|
|
|
@ -6,10 +6,10 @@ use tiraya_utils::config::CONFIG;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
artist::{ArtistId, ArtistJsonb},
|
artist::{ArtistId, ArtistJsonb},
|
||||||
map_artists, AlbumType, DatePrecision, Id, MusicService, SrcId, SrcIdOwned, TrackSlim,
|
AlbumType, DatePrecision, Id, IdLike, MusicService, SrcId, SrcIdOwned, TrackSlim, TrackSlimRow,
|
||||||
TrackSlimRow,
|
|
||||||
};
|
};
|
||||||
use crate::error::{DatabaseError, OptionalRes};
|
use crate::error::{DatabaseError, OptionalRes};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, FromRow)]
|
#[derive(Debug, Serialize, FromRow)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
|
@ -150,11 +150,11 @@ impl Album {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
AlbumRow,
|
AlbumRow,
|
||||||
|
@ -194,7 +194,7 @@ group by b.id"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
.map(Album::from)
|
.map(Album::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,11 +239,11 @@ group by b.id"#,
|
||||||
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_id<'a, E>(id: Id<'_>, exec: E) -> Result<i32, DatabaseError>
|
pub async fn resolve_id<'a, E>(id: impl IdLike, exec: E) -> Result<i32, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => Ok(id),
|
Id::Db(id) => Ok(id),
|
||||||
Id::Src(src_id, srv) => {
|
Id::Src(src_id, srv) => {
|
||||||
let srcid = SrcId(src_id, srv);
|
let srcid = SrcId(src_id, srv);
|
||||||
|
@ -402,7 +402,7 @@ returning id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlbumUpdate<'_> {
|
impl AlbumUpdate<'_> {
|
||||||
pub async fn update<'a, E>(&self, id: Id<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn update<'a, E>(&self, id: impl IdLike, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
@ -497,7 +497,7 @@ impl AlbumUpdate<'_> {
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
query.push(" where ");
|
query.push(" where ");
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
query.push("id=");
|
query.push("id=");
|
||||||
query.push_bind(id);
|
query.push_bind(id);
|
||||||
|
@ -517,11 +517,11 @@ impl AlbumUpdate<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlbumSlim {
|
impl AlbumSlim {
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
let row: AlbumSlimRow = match id {
|
let row: AlbumSlimRow = match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
AlbumSlimRow,
|
AlbumSlimRow,
|
||||||
|
@ -559,7 +559,7 @@ group by b.id"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))?;
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))?;
|
||||||
|
|
||||||
Ok(row.into())
|
Ok(row.into())
|
||||||
}
|
}
|
||||||
|
@ -583,7 +583,7 @@ impl From<AlbumRow> for Album {
|
||||||
src_id: value.src_id,
|
src_id: value.src_id,
|
||||||
service: value.service,
|
service: value.service,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
artists: map_artists(value.artists, value.ul_artists),
|
artists: util::map_artists(value.artists, value.ul_artists),
|
||||||
release_date: value.release_date,
|
release_date: value.release_date,
|
||||||
release_date_precision: value.release_date_precision,
|
release_date_precision: value.release_date_precision,
|
||||||
album_type: value.album_type,
|
album_type: value.album_type,
|
||||||
|
@ -604,7 +604,7 @@ impl From<AlbumSlimRow> for AlbumSlim {
|
||||||
src_id: value.src_id,
|
src_id: value.src_id,
|
||||||
service: value.service,
|
service: value.service,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
artists: map_artists(value.artists, value.ul_artists),
|
artists: util::map_artists(value.artists, value.ul_artists),
|
||||||
release_date: value.release_date,
|
release_date: value.release_date,
|
||||||
album_type: value.album_type,
|
album_type: value.album_type,
|
||||||
image_url: value.image_url,
|
image_url: value.image_url,
|
||||||
|
|
|
@ -5,8 +5,8 @@ use time::PrimitiveDateTime;
|
||||||
use tiraya_utils::config::CONFIG;
|
use tiraya_utils::config::CONFIG;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
album::AlbumSlimRow, AlbumSlim, Id, IdOwned, InternalId, MusicService, PlaylistSlim, SrcId,
|
album::AlbumSlimRow, AlbumSlim, Id, IdLike, IdOwned, InternalId, MusicService, PlaylistSlim,
|
||||||
SrcIdOwned, SyncData, TrackSlim, TrackSlimRow, TrackTiny,
|
SrcId, SrcIdOwned, SyncData, TrackSlim, TrackSlimRow, TrackTiny,
|
||||||
};
|
};
|
||||||
use crate::error::{DatabaseError, OptionalRes};
|
use crate::error::{DatabaseError, OptionalRes};
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ pub struct ArtistJsonb {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Artist identifier
|
/// Artist identifier
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ArtistId {
|
pub struct ArtistId {
|
||||||
pub id: Option<SrcIdOwned>,
|
pub id: Option<SrcIdOwned>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -103,11 +103,11 @@ impl Artist {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
|
@ -161,7 +161,7 @@ where aa.src_id=$1 and aa.service=$2"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
async fn _get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
||||||
|
@ -258,10 +258,10 @@ where aa.src_id=$1 and aa.service=$2"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_id(
|
pub async fn resolve_id(
|
||||||
id: Id<'_>,
|
id: impl IdLike,
|
||||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<i32, DatabaseError> {
|
) -> Result<i32, DatabaseError> {
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => Ok(id),
|
Id::Db(id) => Ok(id),
|
||||||
Id::Src(src_id, srv) => {
|
Id::Src(src_id, srv) => {
|
||||||
let srcid = SrcId(src_id, srv);
|
let srcid = SrcId(src_id, srv);
|
||||||
|
@ -273,10 +273,10 @@ where aa.src_id=$1 and aa.service=$2"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resolve_both_ids(
|
async fn resolve_both_ids(
|
||||||
id: Id<'_>,
|
id: impl IdLike,
|
||||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<(i32, SrcIdOwned), DatabaseError> {
|
) -> Result<(i32, SrcIdOwned), DatabaseError> {
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
let srcid = Self::get_src_id(id, &mut **tx)
|
let srcid = Self::get_src_id(id, &mut **tx)
|
||||||
.await?
|
.await?
|
||||||
|
@ -310,8 +310,8 @@ on conflict (src_id, service) do update set artist_id=excluded.artist_id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn merge(
|
pub async fn merge(
|
||||||
id_prim: Id<'_>,
|
id_prim: impl IdLike,
|
||||||
id_sec: Id<'_>,
|
id_sec: impl IdLike,
|
||||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<(), DatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
let id_prim = Self::resolve_id(id_prim, tx).await?;
|
let id_prim = Self::resolve_id(id_prim, tx).await?;
|
||||||
|
@ -687,7 +687,7 @@ returning id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtistUpdate<'_> {
|
impl ArtistUpdate<'_> {
|
||||||
pub async fn update<'a, E>(&self, id: Id<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn update<'a, E>(&self, id: impl IdLike, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
@ -782,7 +782,7 @@ impl ArtistUpdate<'_> {
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
query.push(" where ");
|
query.push(" where ");
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
query.push("id=");
|
query.push("id=");
|
||||||
query.push_bind(id);
|
query.push_bind(id);
|
||||||
|
@ -802,11 +802,11 @@ impl ArtistUpdate<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArtistSlim {
|
impl ArtistSlim {
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => sqlx::query_as!(
|
Id::Db(id) => sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
r#"select src_id, service as "service: _", name, image_url, image_hash from artists where id=$1"#,
|
r#"select src_id, service as "service: _", name, image_url, image_hash from artists where id=$1"#,
|
||||||
|
@ -824,7 +824,7 @@ where src_id=$1 and service=$2"#,
|
||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
.await,
|
.await,
|
||||||
}?
|
}?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_vec<'a, E>(ids: &[i32], exec: E) -> Result<Vec<Option<Self>>, DatabaseError>
|
pub async fn get_vec<'a, E>(ids: &[i32], exec: E) -> Result<Vec<Option<Self>>, DatabaseError>
|
||||||
|
|
|
@ -107,14 +107,14 @@ pub enum SyncData {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum SyncError {
|
pub enum SyncError {
|
||||||
NotFound,
|
Unavailable,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncError {
|
impl SyncError {
|
||||||
pub fn retry(&self, age: Duration) -> bool {
|
pub fn retry(&self, age: Duration) -> bool {
|
||||||
match self {
|
match self {
|
||||||
SyncError::NotFound => age > Duration::days(7),
|
SyncError::Unavailable => age > Duration::days(7),
|
||||||
SyncError::Other => true,
|
SyncError::Other => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,12 +138,12 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let err2 = SyncData::Error {
|
let err2 = SyncData::Error {
|
||||||
typ: SyncError::NotFound,
|
typ: SyncError::Unavailable,
|
||||||
msg: "gone".to_string(),
|
msg: "gone".to_string(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serde_json::to_string(&err2).unwrap(),
|
serde_json::to_string(&err2).unwrap(),
|
||||||
r#"{"error":{"typ":"not_found","msg":"gone"}}"#
|
r#"{"error":{"typ":"unavailable","msg":"gone"}}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
let date = SyncData::Date(datetime!(2023-05-12 18:00 UTC));
|
let date = SyncData::Date(datetime!(2023-05-12 18:00 UTC));
|
||||||
|
|
276
crates/db/src/models/id.rs
Normal file
276
crates/db/src/models/id.rs
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
use std::{fmt::Write, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{de::Visitor, Deserialize, Serialize};
|
||||||
|
use sqlx::{FromRow, Row};
|
||||||
|
|
||||||
|
use crate::error::DatabaseError;
|
||||||
|
|
||||||
|
use super::MusicService;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Id<'a> {
|
||||||
|
Db(i32),
|
||||||
|
Src(&'a str, MusicService),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum IdOwned {
|
||||||
|
Db(i32),
|
||||||
|
Src(String, MusicService),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SrcId<'a>(pub &'a str, pub MusicService);
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SrcIdOwned(pub String, pub MusicService);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, FromRow)]
|
||||||
|
pub struct InternalId {
|
||||||
|
pub id: i32,
|
||||||
|
pub src_id: String,
|
||||||
|
pub service: MusicService,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdOwned {
|
||||||
|
pub fn as_id(&self) -> Id<'_> {
|
||||||
|
match self {
|
||||||
|
IdOwned::Db(id) => Id::Db(*id),
|
||||||
|
IdOwned::Src(src_id, srv) => Id::Src(src_id.as_str(), *srv),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SrcId<'_> {
|
||||||
|
pub fn to_owned(&self) -> SrcIdOwned {
|
||||||
|
SrcIdOwned(self.0.to_owned(), self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SrcIdOwned {
|
||||||
|
pub fn as_srcid(&self) -> SrcId<'_> {
|
||||||
|
SrcId(&self.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Id<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Id::Db(id) => id.fmt(f),
|
||||||
|
Id::Src(src_id, srv) => {
|
||||||
|
srv.fmt(f)?;
|
||||||
|
f.write_char(':')?;
|
||||||
|
src_id.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IdOwned {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IdOwned::Db(id) => id.fmt(f),
|
||||||
|
IdOwned::Src(src_id, srv) => {
|
||||||
|
srv.fmt(f)?;
|
||||||
|
f.write_char(':')?;
|
||||||
|
src_id.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SrcId<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.1.fmt(f)?;
|
||||||
|
f.write_char(':')?;
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SrcIdOwned {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.1.fmt(f)?;
|
||||||
|
f.write_char(':')?;
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SrcId<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_char('"')?;
|
||||||
|
std::fmt::Display::fmt(&self, f)?;
|
||||||
|
f.write_char('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SrcIdOwned {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_char('"')?;
|
||||||
|
std::fmt::Display::fmt(&self, f)?;
|
||||||
|
f.write_char('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for SrcIdOwned {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.to_string().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for SrcIdOwned {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct SrcIdVisitor;
|
||||||
|
const EXPECT: &str = "a Tiraya source id";
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for SrcIdVisitor {
|
||||||
|
type Value = SrcIdOwned;
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str(EXPECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
parse_src_id(v).ok_or(E::invalid_value(serde::de::Unexpected::Str(v), &EXPECT))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(SrcIdVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for SrcIdOwned {
|
||||||
|
fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
|
||||||
|
Ok(Self(row.try_get("src_id")?, row.try_get("service")?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialEq<IdOwned> for Id<'a> {
|
||||||
|
fn eq(&self, other: &IdOwned) -> bool {
|
||||||
|
match self {
|
||||||
|
Id::Db(id) => match other {
|
||||||
|
IdOwned::Db(o_id) => id == o_id,
|
||||||
|
IdOwned::Src(_, _) => false,
|
||||||
|
},
|
||||||
|
Id::Src(src_id, srv) => match other {
|
||||||
|
IdOwned::Db(_) => false,
|
||||||
|
IdOwned::Src(o_src_id, o_srv) => src_id == o_src_id && srv == o_srv,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialEq<Id<'a>> for IdOwned {
|
||||||
|
fn eq(&self, other: &Id<'a>) -> bool {
|
||||||
|
other.eq(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialEq<SrcIdOwned> for SrcId<'a> {
|
||||||
|
fn eq(&self, other: &SrcIdOwned) -> bool {
|
||||||
|
self.0 == other.0 && self.1 == other.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PartialEq<SrcId<'a>> for SrcIdOwned {
|
||||||
|
fn eq(&self, other: &SrcId<'a>) -> bool {
|
||||||
|
other.eq(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SrcIdOwned> for IdOwned {
|
||||||
|
fn from(value: SrcIdOwned) -> Self {
|
||||||
|
Self::Src(value.0, value.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InternalId> for IdOwned {
|
||||||
|
fn from(value: InternalId) -> Self {
|
||||||
|
Self::Db(value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InternalId> for SrcIdOwned {
|
||||||
|
fn from(value: InternalId) -> Self {
|
||||||
|
SrcIdOwned(value.src_id, value.service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SrcIdOwned {
|
||||||
|
type Err = DatabaseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
parse_src_id(s).ok_or(DatabaseError::Other("invalid srcid".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_src_id(id: &str) -> Option<SrcIdOwned> {
|
||||||
|
let (srv_str, src_id) = id.split_once(':')?;
|
||||||
|
let service = serde_plain::from_str::<MusicService>(srv_str).ok()?;
|
||||||
|
Some(SrcIdOwned(src_id.to_owned(), service))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IdLike {
|
||||||
|
fn id(&self) -> Id<'_>;
|
||||||
|
fn to_owned_id(&self) -> IdOwned;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdLike for Id<'_> {
|
||||||
|
fn id(&self) -> Id<'_> {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_owned_id(&self) -> IdOwned {
|
||||||
|
match self {
|
||||||
|
Id::Db(id) => IdOwned::Db(*id),
|
||||||
|
Id::Src(src_id, srv) => IdOwned::Src(src_id.to_string(), *srv),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdLike for IdOwned {
|
||||||
|
fn id(&self) -> Id<'_> {
|
||||||
|
self.as_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_owned_id(&self) -> IdOwned {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdLike for SrcId<'_> {
|
||||||
|
fn id(&self) -> Id<'_> {
|
||||||
|
Id::Src(self.0, self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_owned_id(&self) -> IdOwned {
|
||||||
|
IdOwned::Src(self.0.to_owned(), self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdLike for SrcIdOwned {
|
||||||
|
fn id(&self) -> Id<'_> {
|
||||||
|
Id::Src(&self.0, self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_owned_id(&self) -> IdOwned {
|
||||||
|
IdOwned::Src(self.0.to_owned(), self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdLike for i32 {
|
||||||
|
fn id(&self) -> Id<'_> {
|
||||||
|
Id::Db(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_owned_id(&self) -> IdOwned {
|
||||||
|
IdOwned::Db(*self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,8 @@
|
||||||
use std::{fmt::Write, str::FromStr};
|
|
||||||
|
|
||||||
use serde::{de::Visitor, Deserialize, Serialize};
|
|
||||||
use sqlx::{types::Json, FromRow, Row};
|
|
||||||
|
|
||||||
mod album;
|
mod album;
|
||||||
mod artist;
|
mod artist;
|
||||||
mod change_operation;
|
mod change_operation;
|
||||||
mod enums;
|
mod enums;
|
||||||
|
mod id;
|
||||||
mod playlist;
|
mod playlist;
|
||||||
mod playlist_change;
|
mod playlist_change;
|
||||||
mod track;
|
mod track;
|
||||||
|
@ -18,268 +14,17 @@ pub use enums::{
|
||||||
AlbumType, DatePrecision, EntityType, MusicService, PlaylistImgType, PlaylistType, SyncData,
|
AlbumType, DatePrecision, EntityType, MusicService, PlaylistImgType, PlaylistType, SyncData,
|
||||||
SyncError, UserType,
|
SyncError, UserType,
|
||||||
};
|
};
|
||||||
|
pub use id::{Id, IdLike, IdOwned, InternalId, SrcId, SrcIdOwned};
|
||||||
pub use playlist::{
|
pub use playlist::{
|
||||||
Playlist, PlaylistEntry, PlaylistEntryTrack, PlaylistNew, PlaylistSlim, PlaylistUpdate,
|
Playlist, PlaylistEntry, PlaylistEntryTrack, PlaylistNew, PlaylistSlim, PlaylistUpdate,
|
||||||
};
|
};
|
||||||
pub use track::{Track, TrackNew, TrackSlim, TrackTiny, TrackUpdate};
|
pub use track::{
|
||||||
|
Track, TrackMatchAlbum, TrackMatchData, TrackMatchMeta, TrackNew, TrackSlim, TrackTiny,
|
||||||
|
TrackUpdate,
|
||||||
|
};
|
||||||
pub use user::{User, UserItem, UserNew, UserUpdate};
|
pub use user::{User, UserItem, UserNew, UserUpdate};
|
||||||
|
|
||||||
|
pub(crate) use artist::ArtistJsonb;
|
||||||
pub(crate) use change_operation::ChangeOperation;
|
pub(crate) use change_operation::ChangeOperation;
|
||||||
pub(crate) use playlist_change::{PlaylistChange, PlaylistChangeAdd, PlaylistChangeNew};
|
pub(crate) use playlist_change::{PlaylistChange, PlaylistChangeAdd, PlaylistChangeNew};
|
||||||
pub(crate) use track::TrackSlimRow;
|
pub(crate) use track::TrackSlimRow;
|
||||||
|
|
||||||
use artist::ArtistJsonb;
|
|
||||||
|
|
||||||
use crate::error::DatabaseError;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum Id<'a> {
|
|
||||||
Db(i32),
|
|
||||||
Src(&'a str, MusicService),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum IdOwned {
|
|
||||||
Db(i32),
|
|
||||||
Src(String, MusicService),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct SrcId<'a>(pub &'a str, pub MusicService);
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct SrcIdOwned(pub String, pub MusicService);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, FromRow)]
|
|
||||||
pub struct InternalId {
|
|
||||||
pub id: i32,
|
|
||||||
pub src_id: String,
|
|
||||||
pub service: MusicService,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Id<'_> {
|
|
||||||
pub fn to_owned(&self) -> IdOwned {
|
|
||||||
match self {
|
|
||||||
Id::Db(id) => IdOwned::Db(*id),
|
|
||||||
Id::Src(src_id, srv) => IdOwned::Src(src_id.to_string(), *srv),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IdOwned {
|
|
||||||
pub fn as_id(&self) -> Id<'_> {
|
|
||||||
match self {
|
|
||||||
IdOwned::Db(id) => Id::Db(*id),
|
|
||||||
IdOwned::Src(src_id, srv) => Id::Src(src_id.as_str(), *srv),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SrcId<'_> {
|
|
||||||
pub fn to_owned(&self) -> SrcIdOwned {
|
|
||||||
SrcIdOwned(self.0.to_owned(), self.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_owned_id(&self) -> IdOwned {
|
|
||||||
IdOwned::Src(self.0.to_owned(), self.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> Id {
|
|
||||||
Id::Src(self.0, self.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SrcIdOwned {
|
|
||||||
pub fn as_srcid(&self) -> SrcId<'_> {
|
|
||||||
SrcId(&self.0, self.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> Id {
|
|
||||||
Id::Src(&self.0, self.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Id<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Id::Db(id) => id.fmt(f),
|
|
||||||
Id::Src(src_id, srv) => {
|
|
||||||
srv.fmt(f)?;
|
|
||||||
f.write_char(':')?;
|
|
||||||
src_id.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for IdOwned {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
IdOwned::Db(id) => id.fmt(f),
|
|
||||||
IdOwned::Src(src_id, srv) => {
|
|
||||||
srv.fmt(f)?;
|
|
||||||
f.write_char(':')?;
|
|
||||||
src_id.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for SrcId<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.1.fmt(f)?;
|
|
||||||
f.write_char(':')?;
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for SrcIdOwned {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.1.fmt(f)?;
|
|
||||||
f.write_char(':')?;
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for SrcId<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_char('"')?;
|
|
||||||
std::fmt::Display::fmt(&self, f)?;
|
|
||||||
f.write_char('"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for SrcIdOwned {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_char('"')?;
|
|
||||||
std::fmt::Display::fmt(&self, f)?;
|
|
||||||
f.write_char('"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for SrcIdOwned {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
self.to_string().serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for SrcIdOwned {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct SrcIdVisitor;
|
|
||||||
const EXPECT: &str = "a Tiraya source id";
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for SrcIdVisitor {
|
|
||||||
type Value = SrcIdOwned;
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str(EXPECT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
parse_src_id(v).ok_or(E::invalid_value(serde::de::Unexpected::Str(v), &EXPECT))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_str(SrcIdVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for SrcIdOwned {
|
|
||||||
fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
|
|
||||||
Ok(Self(row.try_get("src_id")?, row.try_get("service")?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PartialEq<IdOwned> for Id<'a> {
|
|
||||||
fn eq(&self, other: &IdOwned) -> bool {
|
|
||||||
match self {
|
|
||||||
Id::Db(id) => match other {
|
|
||||||
IdOwned::Db(o_id) => id == o_id,
|
|
||||||
IdOwned::Src(_, _) => false,
|
|
||||||
},
|
|
||||||
Id::Src(src_id, srv) => match other {
|
|
||||||
IdOwned::Db(_) => false,
|
|
||||||
IdOwned::Src(o_src_id, o_srv) => src_id == o_src_id && srv == o_srv,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PartialEq<Id<'a>> for IdOwned {
|
|
||||||
fn eq(&self, other: &Id<'a>) -> bool {
|
|
||||||
other.eq(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PartialEq<SrcIdOwned> for SrcId<'a> {
|
|
||||||
fn eq(&self, other: &SrcIdOwned) -> bool {
|
|
||||||
self.0 == other.0 && self.1 == other.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PartialEq<SrcId<'a>> for SrcIdOwned {
|
|
||||||
fn eq(&self, other: &SrcId<'a>) -> bool {
|
|
||||||
other.eq(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SrcIdOwned> for IdOwned {
|
|
||||||
fn from(value: SrcIdOwned) -> Self {
|
|
||||||
Self::Src(value.0, value.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InternalId> for IdOwned {
|
|
||||||
fn from(value: InternalId) -> Self {
|
|
||||||
Self::Db(value.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InternalId> for SrcIdOwned {
|
|
||||||
fn from(value: InternalId) -> Self {
|
|
||||||
SrcIdOwned(value.src_id, value.service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn map_artists(
|
|
||||||
jsonb: Option<Json<Vec<ArtistJsonb>>>,
|
|
||||||
ul: Option<Vec<String>>,
|
|
||||||
) -> Vec<ArtistId> {
|
|
||||||
let mut artists = Vec::new();
|
|
||||||
if let Some(jsonb) = jsonb {
|
|
||||||
for a in jsonb.0 {
|
|
||||||
artists.push(ArtistId {
|
|
||||||
id: Some(SrcIdOwned(a.src_id, a.service)),
|
|
||||||
name: a.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(ul) = ul {
|
|
||||||
for a in ul {
|
|
||||||
artists.push(ArtistId { id: None, name: a });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
artists
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_src_id(id: &str) -> Option<SrcIdOwned> {
|
|
||||||
let (srv_str, src_id) = id.split_once(':')?;
|
|
||||||
let service = serde_plain::from_str::<MusicService>(srv_str).ok()?;
|
|
||||||
Some(SrcIdOwned(src_id.to_owned(), service))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SrcIdOwned {
|
|
||||||
type Err = DatabaseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
parse_src_id(s).ok_or(DatabaseError::Other("invalid srcid".into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ use tiraya_utils::config::CONFIG;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
playlist_change::PlaylistChangeAdd, Id, MusicService, PlaylistChange, PlaylistChangeNew,
|
playlist_change::PlaylistChangeAdd, Id, IdLike, MusicService, PlaylistChange,
|
||||||
PlaylistImgType, PlaylistType, SrcId, SrcIdOwned, SyncData, TrackSlim,
|
PlaylistChangeNew, PlaylistImgType, PlaylistType, SrcId, SrcIdOwned, SyncData, TrackSlim,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{DatabaseError, OptionalRes},
|
error::{DatabaseError, OptionalRes},
|
||||||
|
@ -106,11 +106,11 @@ impl Playlist {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
|
@ -138,7 +138,7 @@ from playlists where src_id=$1 and service=$2"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
pub async fn get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
||||||
|
@ -168,11 +168,11 @@ from playlists where src_id=$1 and service=$2"#,
|
||||||
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_id<'a, E>(id: Id<'_>, exec: E) -> Result<i32, DatabaseError>
|
pub async fn resolve_id<'a, E>(id: impl IdLike, exec: E) -> Result<i32, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => Ok(id),
|
Id::Db(id) => Ok(id),
|
||||||
Id::Src(src_id, srv) => {
|
Id::Src(src_id, srv) => {
|
||||||
let srcid = SrcId(src_id, srv);
|
let srcid = SrcId(src_id, srv);
|
||||||
|
@ -560,7 +560,7 @@ returning id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlaylistUpdate<'_> {
|
impl PlaylistUpdate<'_> {
|
||||||
pub async fn update<'a, E>(&self, id: Id<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn update<'a, E>(&self, id: impl IdLike, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
@ -615,7 +615,7 @@ impl PlaylistUpdate<'_> {
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
query.push(" where ");
|
query.push(" where ");
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
query.push("id=");
|
query.push("id=");
|
||||||
query.push_bind(id);
|
query.push_bind(id);
|
||||||
|
@ -639,11 +639,11 @@ impl PlaylistSlim {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => sqlx::query_as!(
|
Id::Db(id) => sqlx::query_as!(
|
||||||
PlaylistSlim,
|
PlaylistSlim,
|
||||||
r#"select p.src_id, p.service as "service: _", p.name, p.image_url, p.image_hash,
|
r#"select p.src_id, p.service as "service: _", p.name, p.image_url, p.image_hash,
|
||||||
|
@ -666,7 +666,7 @@ where p.src_id=$1 and p.service=$2"#,
|
||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
.await,
|
.await,
|
||||||
}?
|
}?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_vec<'a, E>(ids: &[i32], exec: E) -> Result<Vec<Option<Self>>, DatabaseError>
|
pub async fn get_vec<'a, E>(ids: &[i32], exec: E) -> Result<Vec<Option<Self>>, DatabaseError>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use futures::{stream, StreamExt, TryStreamExt};
|
use futures::{stream, StreamExt, TryStreamExt};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{types::Json, FromRow, QueryBuilder};
|
use sqlx::{types::Json, FromRow, QueryBuilder};
|
||||||
use time::{Date, PrimitiveDateTime};
|
use time::{Date, PrimitiveDateTime};
|
||||||
use tiraya_utils::config::CONFIG;
|
use tiraya_utils::config::CONFIG;
|
||||||
|
@ -7,9 +7,10 @@ use tiraya_utils::config::CONFIG;
|
||||||
use super::{
|
use super::{
|
||||||
album::AlbumId,
|
album::AlbumId,
|
||||||
artist::{ArtistId, ArtistJsonb},
|
artist::{ArtistId, ArtistJsonb},
|
||||||
map_artists, AlbumType, Id, MusicService, SrcId, SrcIdOwned,
|
AlbumType, Id, IdLike, MusicService, SrcId, SrcIdOwned,
|
||||||
};
|
};
|
||||||
use crate::error::{DatabaseError, OptionalRes};
|
use crate::error::{DatabaseError, OptionalRes};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
|
@ -133,6 +134,35 @@ pub struct TrackTiny {
|
||||||
pub duration: Option<i32>,
|
pub duration: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information stored with an automatically matched track
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TrackMatchData {
|
||||||
|
pub matchmaker_version: u16,
|
||||||
|
pub matchmaker_cfg: String,
|
||||||
|
pub score: f32,
|
||||||
|
pub original: TrackMatchMeta,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub closest: Option<TrackMatchMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TrackMatchMeta {
|
||||||
|
pub id: SrcIdOwned,
|
||||||
|
pub name: String,
|
||||||
|
pub artists: Vec<ArtistId>,
|
||||||
|
pub album: TrackMatchAlbum,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub isrc: Option<String>,
|
||||||
|
pub duration: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TrackMatchAlbum {
|
||||||
|
pub id: Option<SrcIdOwned>,
|
||||||
|
pub name: String,
|
||||||
|
pub image_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Track {
|
impl Track {
|
||||||
pub fn id(&self) -> Id {
|
pub fn id(&self) -> Id {
|
||||||
Id::Db(self.id)
|
Id::Db(self.id)
|
||||||
|
@ -142,11 +172,11 @@ impl Track {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
TrackRow,
|
TrackRow,
|
||||||
|
@ -211,7 +241,7 @@ group by t.id"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
.map(Track::from)
|
.map(Track::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,10 +342,10 @@ where ta.src_id=$1 and ta.service=$2"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_id(
|
pub async fn resolve_id(
|
||||||
id: Id<'_>,
|
id: impl IdLike,
|
||||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<i32, DatabaseError> {
|
) -> Result<i32, DatabaseError> {
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => Ok(id),
|
Id::Db(id) => Ok(id),
|
||||||
Id::Src(src_id, srv) => {
|
Id::Src(src_id, srv) => {
|
||||||
let srcid = SrcId(src_id, srv);
|
let srcid = SrcId(src_id, srv);
|
||||||
|
@ -326,16 +356,29 @@ where ta.src_id=$1 and ta.service=$2"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_alias<'a, E>(id: i32, alias: SrcId<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn add_alias<'a, E>(
|
||||||
|
alias: SrcId<'_>,
|
||||||
|
track_id: Option<i32>,
|
||||||
|
match_data: Option<&TrackMatchData>,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
let match_data_json = match match_data {
|
||||||
|
Some(match_data) => Some(serde_json::to_value(match_data)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"insert into track_aliases (src_id, service, track_id) values ($1, $2, $3)
|
r#"insert into track_aliases (src_id, service, track_id, match_data) values ($1, $2, $3, $4)
|
||||||
on conflict (src_id, service) do update set track_id=excluded.track_id"#,
|
on conflict (src_id, service) do update set
|
||||||
|
track_id = coalesce(excluded.track_id, track_aliases.track_id),
|
||||||
|
match_data = coalesce(excluded.match_data, track_aliases.match_data)"#,
|
||||||
alias.0,
|
alias.0,
|
||||||
alias.1 as MusicService,
|
alias.1 as MusicService,
|
||||||
id,
|
track_id,
|
||||||
|
match_data_json,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -384,7 +427,7 @@ returning id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackUpdate<'_> {
|
impl TrackUpdate<'_> {
|
||||||
pub async fn update<'a, E>(&self, id: Id<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn update<'a, E>(&self, id: impl IdLike, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
@ -495,7 +538,7 @@ impl TrackUpdate<'_> {
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
query.push(" where ");
|
query.push(" where ");
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
query.push("id=");
|
query.push("id=");
|
||||||
query.push_bind(id);
|
query.push_bind(id);
|
||||||
|
@ -519,12 +562,12 @@ impl TrackSlim {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
let row: TrackSlimRow =
|
let row: TrackSlimRow =
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => sqlx::query_as!(
|
Id::Db(id) => sqlx::query_as!(
|
||||||
TrackSlimRow,
|
TrackSlimRow,
|
||||||
r#"select t.src_id, t.service as "service: _", t.name, t.duration, t.album_pos,
|
r#"select t.src_id, t.service as "service: _", t.name, t.duration, t.album_pos,
|
||||||
|
@ -593,7 +636,7 @@ group by (t.id, b.id)"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))?;
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))?;
|
||||||
|
|
||||||
Ok(row.into())
|
Ok(row.into())
|
||||||
}
|
}
|
||||||
|
@ -618,7 +661,7 @@ impl From<TrackRow> for Track {
|
||||||
service: value.service,
|
service: value.service,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
duration: value.duration,
|
duration: value.duration,
|
||||||
artists: map_artists(value.artists, value.ul_artists),
|
artists: util::map_artists(value.artists, value.ul_artists),
|
||||||
size: value.size,
|
size: value.size,
|
||||||
loudness: value.loudness,
|
loudness: value.loudness,
|
||||||
album_id: value.album_id,
|
album_id: value.album_id,
|
||||||
|
@ -642,7 +685,7 @@ impl From<TrackSlimRow> for TrackSlim {
|
||||||
service: value.service,
|
service: value.service,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
duration: value.duration,
|
duration: value.duration,
|
||||||
artists: map_artists(value.artists, value.ul_artists),
|
artists: util::map_artists(value.artists, value.ul_artists),
|
||||||
album: AlbumId {
|
album: AlbumId {
|
||||||
src_id: value.album_src_id,
|
src_id: value.album_src_id,
|
||||||
service: value.album_service,
|
service: value.album_service,
|
||||||
|
@ -778,7 +821,7 @@ mod tests {
|
||||||
|
|
||||||
let src_id = SrcId("1t2qYCAjUAoGfeFeoBlK51", MusicService::Spotify);
|
let src_id = SrcId("1t2qYCAjUAoGfeFeoBlK51", MusicService::Spotify);
|
||||||
|
|
||||||
Track::add_alias(ids::TRACK_ID_BMAMBA, src_id, &pool)
|
Track::add_alias(src_id, Some(ids::TRACK_ID_BMAMBA), None, &pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let track_id = Track::get_id(src_id, &pool).await.unwrap().unwrap();
|
let track_id = Track::get_id(src_id, &pool).await.unwrap().unwrap();
|
||||||
|
|
|
@ -4,7 +4,7 @@ use time::PrimitiveDateTime;
|
||||||
|
|
||||||
use crate::error::DatabaseError;
|
use crate::error::DatabaseError;
|
||||||
|
|
||||||
use super::{enums::UserType, Id, MusicService, PlaylistSlim, SrcId, SrcIdOwned, SyncData};
|
use super::{enums::UserType, Id, IdLike, MusicService, PlaylistSlim, SrcId, SrcIdOwned, SyncData};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, FromRow)]
|
#[derive(Debug, Serialize, FromRow)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
@ -59,11 +59,11 @@ impl User {
|
||||||
SrcId(&self.src_id, self.service)
|
SrcId(&self.src_id, self.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a, E>(id: Id<'_>, exec: E) -> Result<Self, DatabaseError>
|
pub async fn get<'a, E>(id: impl IdLike, exec: E) -> Result<Self, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
|
@ -91,7 +91,7 @@ from users where src_id=$1 and service=$2"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or_else(|| DatabaseError::NotFound(id.to_owned()))
|
.ok_or_else(|| DatabaseError::NotFound(id.to_owned_id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
pub async fn get_id<'a, E>(id: SrcId<'_>, exec: E) -> Result<Option<i32>, DatabaseError>
|
||||||
|
@ -121,11 +121,11 @@ from users where src_id=$1 and service=$2"#,
|
||||||
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
Ok(res.map(|res| SrcIdOwned(res.src_id, res.service)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_id<'a, E>(id: Id<'_>, exec: E) -> Result<i32, DatabaseError>
|
pub async fn resolve_id<'a, E>(id: impl IdLike, exec: E) -> Result<i32, DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => Ok(id),
|
Id::Db(id) => Ok(id),
|
||||||
Id::Src(src_id, srv) => {
|
Id::Src(src_id, srv) => {
|
||||||
let srcid = SrcId(src_id, srv);
|
let srcid = SrcId(src_id, srv);
|
||||||
|
@ -270,7 +270,7 @@ returning id"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserUpdate<'_> {
|
impl UserUpdate<'_> {
|
||||||
pub async fn update<'a, E>(&self, id: Id<'_>, exec: E) -> Result<(), DatabaseError>
|
pub async fn update<'a, E>(&self, id: impl IdLike, exec: E) -> Result<(), DatabaseError>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
{
|
{
|
||||||
|
@ -301,7 +301,7 @@ impl UserUpdate<'_> {
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
query.push(" where ");
|
query.push(" where ");
|
||||||
match id {
|
match id.id() {
|
||||||
Id::Db(id) => {
|
Id::Db(id) => {
|
||||||
query.push("id=");
|
query.push("id=");
|
||||||
query.push_bind(id);
|
query.push_bind(id);
|
||||||
|
|
|
@ -3,10 +3,13 @@ use std::hash::Hasher;
|
||||||
use hex_literal::hex;
|
use hex_literal::hex;
|
||||||
use similar::algorithms::DiffHook;
|
use similar::algorithms::DiffHook;
|
||||||
use siphasher::sip128::{Hasher128, SipHasher};
|
use siphasher::sip128::{Hasher128, SipHasher};
|
||||||
|
use sqlx::types::Json;
|
||||||
use time::{OffsetDateTime, PrimitiveDateTime};
|
use time::{OffsetDateTime, PrimitiveDateTime};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{ChangeOperation, PlaylistChangeAdd, PlaylistEntry};
|
use crate::models::{
|
||||||
|
ArtistId, ArtistJsonb, ChangeOperation, PlaylistChangeAdd, PlaylistEntry, SrcIdOwned,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn primitive_now() -> PrimitiveDateTime {
|
pub fn primitive_now() -> PrimitiveDateTime {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
|
@ -159,6 +162,27 @@ pub fn diff_playlist(
|
||||||
hook.changes
|
hook.changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_artists(
|
||||||
|
jsonb: Option<Json<Vec<ArtistJsonb>>>,
|
||||||
|
ul: Option<Vec<String>>,
|
||||||
|
) -> Vec<ArtistId> {
|
||||||
|
let mut artists = Vec::new();
|
||||||
|
if let Some(jsonb) = jsonb {
|
||||||
|
for a in jsonb.0 {
|
||||||
|
artists.push(ArtistId {
|
||||||
|
id: Some(SrcIdOwned(a.src_id, a.service)),
|
||||||
|
name: a.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ul) = ul {
|
||||||
|
for a in ul {
|
||||||
|
artists.push(ArtistId { id: None, name: a });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artists
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use insta::assert_ron_snapshot;
|
use insta::assert_ron_snapshot;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use http::StatusCode;
|
||||||
use tiraya_db::models::{EntityType, MusicService, SrcIdOwned};
|
use tiraya_db::models::{EntityType, MusicService, SrcIdOwned};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -15,8 +16,14 @@ pub enum ExtractorError {
|
||||||
id: SrcIdOwned,
|
id: SrcIdOwned,
|
||||||
msg: Cow<'static, str>,
|
msg: Cow<'static, str>,
|
||||||
},
|
},
|
||||||
#[error("could not find a match for track [{id}]")]
|
#[error("{typ} [{id}] not available: {msg}")]
|
||||||
NoMatch { id: SrcIdOwned },
|
Unavailable {
|
||||||
|
typ: EntityType,
|
||||||
|
id: SrcIdOwned,
|
||||||
|
msg: Cow<'static, str>,
|
||||||
|
},
|
||||||
|
#[error("could not find a match for {typ} [{id}]")]
|
||||||
|
NoMatch { typ: EntityType, id: SrcIdOwned },
|
||||||
#[error("no id found")]
|
#[error("no id found")]
|
||||||
NoId,
|
NoId,
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
@ -54,3 +61,50 @@ impl From<rspotify::ClientError> for ExtractorError {
|
||||||
Self::Source(value.into())
|
Self::Source(value.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait MapSrcError<T> {
|
||||||
|
fn mperr(self, typ: EntityType, src_id: &str) -> Result<T, ExtractorError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MapSrcError<T> for Result<T, rustypipe::error::Error> {
|
||||||
|
fn mperr(self, typ: EntityType, src_id: &str) -> Result<T, ExtractorError> {
|
||||||
|
match self {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(error) => Err(match error {
|
||||||
|
rustypipe::error::Error::Extraction(
|
||||||
|
rustypipe::error::ExtractionError::NotFound { .. }
|
||||||
|
| rustypipe::error::ExtractionError::VideoUnavailable { .. },
|
||||||
|
) => ExtractorError::Unavailable {
|
||||||
|
typ,
|
||||||
|
id: SrcIdOwned(src_id.to_owned(), MusicService::YouTube),
|
||||||
|
msg: error.to_string().into(),
|
||||||
|
},
|
||||||
|
_ => ExtractorError::Source(ExtractorSourceError::YouTube(error)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MapSrcError<T> for Result<T, rspotify::ClientError> {
|
||||||
|
fn mperr(self, typ: EntityType, src_id: &str) -> Result<T, ExtractorError> {
|
||||||
|
match self {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(error) => Err(match &error {
|
||||||
|
rspotify::ClientError::Http(http_err) => match http_err.as_ref() {
|
||||||
|
rspotify::http::HttpError::StatusCode(resp) => match resp.status() {
|
||||||
|
StatusCode::NOT_FOUND => ExtractorError::Unavailable {
|
||||||
|
typ,
|
||||||
|
id: SrcIdOwned(src_id.to_owned(), MusicService::Spotify),
|
||||||
|
msg: "not found".into(),
|
||||||
|
},
|
||||||
|
_ => ExtractorError::Source(ExtractorSourceError::Spotify(error)),
|
||||||
|
},
|
||||||
|
rspotify::http::HttpError::Client(_) => {
|
||||||
|
ExtractorError::Source(ExtractorSourceError::Spotify(error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => ExtractorError::Source(ExtractorSourceError::Spotify(error)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -44,6 +44,8 @@ impl std::ops::Deref for Extractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MSG_NOT_EXIST: &str = "does not exist";
|
||||||
|
|
||||||
impl Extractor {
|
impl Extractor {
|
||||||
pub fn new(db: Pool<Postgres>) -> Result<Self, ExtractorError> {
|
pub fn new(db: Pool<Postgres>) -> Result<Self, ExtractorError> {
|
||||||
let core = Arc::new(ExtractorCore::new(db));
|
let core = Arc::new(ExtractorCore::new(db));
|
||||||
|
@ -62,12 +64,17 @@ impl Extractor {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the spotify extractor (if configured)
|
||||||
|
fn sp(&self) -> Result<&SpotifyExtractor, ExtractorError> {
|
||||||
|
self.sp.as_ref().ok_or(ExtractorError::Other(
|
||||||
|
"Spotify integration not configured".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an artist and a list of their albums from the streaming service
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn get_artist(&self, id: SrcId<'_>) -> Result<GetResult<Artist>, ExtractorError> {
|
pub async fn get_artist(&self, id: SrcId<'_>) -> Result<GetResult<Artist>, ExtractorError> {
|
||||||
let artist = Artist::get(id.id(), &self.core.db)
|
let artist = Artist::get(id, &self.core.db).await.to_optional()?;
|
||||||
.await
|
|
||||||
.to_optional()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Get srcid from fetched artist if available (could be an alias)
|
// Get srcid from fetched artist if available (could be an alias)
|
||||||
let srcid = artist
|
let srcid = artist
|
||||||
|
@ -143,25 +150,23 @@ impl Extractor {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
.map_err(ExtractorError::from),
|
.map_err(ExtractorError::from),
|
||||||
Err(e) => match e {
|
Err(e) => {
|
||||||
ExtractorError::Source(e) => {
|
// Store YT-related error (e.g. artist not available)
|
||||||
// Store YT-related error (e.g. artist not available)
|
if let Some(last_update) = last_update {
|
||||||
if let Some(last_update) = last_update {
|
if let Some(sync_data) = util::error_to_sync_data(&e) {
|
||||||
if let Some(sync_data) = util::src_error_to_sync_data(&e) {
|
Artist::set_last_sync(last_update.id, sync_data, &self.core.db).await?;
|
||||||
Artist::set_last_sync(last_update.id, sync_data, &self.core.db).await?;
|
|
||||||
}
|
|
||||||
return Ok(GetResult {
|
|
||||||
c: Artist::get(Id::Db(last_update.id), &self.core.db).await?,
|
|
||||||
status: GetStatus::Error(e),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(ExtractorError::Source(e))
|
return Ok(GetResult {
|
||||||
|
c: Artist::get(Id::Db(last_update.id), &self.core.db).await?,
|
||||||
|
status: GetStatus::Error(Box::new(e)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => Err(e),
|
Err(e)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch all albums of an artist which have not been fetched before and store the tracks
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn fetch_artist_albums(&self, id: i32) -> Result<(), ExtractorError> {
|
pub async fn fetch_artist_albums(&self, id: i32) -> Result<(), ExtractorError> {
|
||||||
let mut n_fetched = 0;
|
let mut n_fetched = 0;
|
||||||
|
@ -257,12 +262,53 @@ impl Extractor {
|
||||||
Ok(GetResult::fetched(res))
|
Ok(GetResult::fetched(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn get_playlist_meta(
|
||||||
|
&self,
|
||||||
|
id: SrcId<'_>,
|
||||||
|
) -> Result<GetResult<Playlist>, ExtractorError> {
|
||||||
|
let playlist = Playlist::get(id, &self.core.db).await.to_optional()?;
|
||||||
|
|
||||||
|
if let Some(playlist) = playlist {
|
||||||
|
Ok(GetResult::stored(playlist))
|
||||||
|
} else {
|
||||||
|
let res = match id.1 {
|
||||||
|
MusicService::YouTube => self.yt.fetch_playlist_meta(id.0).await?,
|
||||||
|
MusicService::Spotify => self.sp()?.fetch_playlist_meta(id.0).await?,
|
||||||
|
MusicService::Tiraya => {
|
||||||
|
return Err(ExtractorError::Unavailable {
|
||||||
|
typ: EntityType::Playlist,
|
||||||
|
id: id.to_owned(),
|
||||||
|
msg: MSG_NOT_EXIST.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ExtractorError::Unsupported {
|
||||||
|
typ: EntityType::Track,
|
||||||
|
srv: id.1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let playlist = Playlist::get(res, &self.core.db).await?;
|
||||||
|
Ok(GetResult::fetched(playlist))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn get_playlist(&self, id: SrcId<'_>) -> Result<GetResult<Playlist>, ExtractorError> {
|
pub async fn get_playlist(&self, id: SrcId<'_>) -> Result<GetResult<Playlist>, ExtractorError> {
|
||||||
let playlist = Playlist::get(id.id(), &self.core.db)
|
let playlist = Playlist::get(id, &self.core.db).await.to_optional()?;
|
||||||
.await
|
|
||||||
.to_optional()
|
if id.1 == MusicService::Tiraya {
|
||||||
.unwrap();
|
return match playlist {
|
||||||
|
Some(playlist) => Ok(GetResult::stored(playlist)),
|
||||||
|
None => Err(ExtractorError::Unavailable {
|
||||||
|
typ: EntityType::Playlist,
|
||||||
|
id: id.to_owned(),
|
||||||
|
msg: MSG_NOT_EXIST.into(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let last_update = if let Some(playlist) = playlist {
|
let last_update = if let Some(playlist) = playlist {
|
||||||
if let Some((last_sync_at, last_sync_data)) = &playlist
|
if let Some((last_sync_at, last_sync_data)) = &playlist
|
||||||
.last_sync_at
|
.last_sync_at
|
||||||
|
@ -306,12 +352,6 @@ impl Extractor {
|
||||||
self.update_playlist(id, last_update.as_ref()).await
|
self.update_playlist(id, last_update.as_ref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sp(&self) -> Result<&SpotifyExtractor, ExtractorError> {
|
|
||||||
self.sp.as_ref().ok_or(ExtractorError::Other(
|
|
||||||
"Spotify integration not configured".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the given playlist from the streaming service,
|
/// Update the given playlist from the streaming service,
|
||||||
/// fetching new tracks if necessary.
|
/// fetching new tracks if necessary.
|
||||||
async fn update_playlist(
|
async fn update_playlist(
|
||||||
|
@ -338,32 +378,38 @@ impl Extractor {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
.map_err(ExtractorError::from),
|
.map_err(ExtractorError::from),
|
||||||
Err(e) => match e {
|
Err(e) => {
|
||||||
ExtractorError::Source(e) => {
|
// Store YT-related error (e.g. playlist not available)
|
||||||
// Store YT-related error (e.g. playlist not available)
|
if let Some(last_update) = last_update {
|
||||||
if let Some(last_update) = last_update {
|
if let Some(sync_data) = util::error_to_sync_data(&e) {
|
||||||
if let Some(sync_data) = util::src_error_to_sync_data(&e) {
|
Playlist::set_last_sync(last_update.id, sync_data, &self.core.db).await?;
|
||||||
Playlist::set_last_sync(last_update.id, sync_data, &self.core.db)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
return Ok(GetResult {
|
|
||||||
c: Playlist::get(Id::Db(last_update.id), &self.core.db).await?,
|
|
||||||
status: GetStatus::Error(e),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(ExtractorError::Source(e))
|
return Ok(GetResult {
|
||||||
|
c: Playlist::get(Id::Db(last_update.id), &self.core.db).await?,
|
||||||
|
status: GetStatus::Error(Box::new(e)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => Err(e),
|
Err(e)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an user from the streaming service, updating its playlists if necessary
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn get_user(&self, id: SrcId<'_>) -> Result<GetResult<User>, ExtractorError> {
|
pub async fn get_user(&self, id: SrcId<'_>) -> Result<GetResult<User>, ExtractorError> {
|
||||||
let user = User::get(id.id(), &self.core.db)
|
let user = User::get(id, &self.core.db).await.to_optional()?;
|
||||||
.await
|
|
||||||
.to_optional()
|
if id.1 == MusicService::Tiraya {
|
||||||
.unwrap();
|
return match user {
|
||||||
|
Some(user) => Ok(GetResult::stored(user)),
|
||||||
|
None => Err(ExtractorError::Unavailable {
|
||||||
|
typ: EntityType::User,
|
||||||
|
id: id.to_owned(),
|
||||||
|
msg: MSG_NOT_EXIST.into(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let last_update = if let Some(user) = user {
|
let last_update = if let Some(user) = user {
|
||||||
if let Some((last_sync_at, last_sync_data)) =
|
if let Some((last_sync_at, last_sync_data)) =
|
||||||
&user.last_sync_at.zip(user.last_sync_data.as_deref())
|
&user.last_sync_at.zip(user.last_sync_data.as_deref())
|
||||||
|
@ -433,23 +479,19 @@ impl Extractor {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
.map_err(ExtractorError::from),
|
.map_err(ExtractorError::from),
|
||||||
Err(e) => match e {
|
Err(e) => {
|
||||||
ExtractorError::Source(e) => {
|
// Store YT-related error (e.g. playlist not available)
|
||||||
// Store YT-related error (e.g. playlist not available)
|
if let Some(last_update) = last_update {
|
||||||
if let Some(last_update) = last_update {
|
if let Some(sync_data) = util::error_to_sync_data(&e) {
|
||||||
if let Some(sync_data) = util::src_error_to_sync_data(&e) {
|
Playlist::set_last_sync(last_update.id, sync_data, &self.core.db).await?;
|
||||||
Playlist::set_last_sync(last_update.id, sync_data, &self.core.db)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
return Ok(GetResult {
|
|
||||||
c: User::get(Id::Db(last_update.id), &self.core.db).await?,
|
|
||||||
status: GetStatus::Error(e),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(ExtractorError::Source(e))
|
return Ok(GetResult {
|
||||||
|
c: User::get(Id::Db(last_update.id), &self.core.db).await?,
|
||||||
|
status: GetStatus::Error(Box::new(e)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => Err(e),
|
Err(e)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use time::OffsetDateTime;
|
||||||
use tiraya_db::models::{SrcIdOwned, SyncData};
|
use tiraya_db::models::{SrcIdOwned, SyncData};
|
||||||
use tiraya_utils::config::CONFIG;
|
use tiraya_utils::config::CONFIG;
|
||||||
|
|
||||||
use crate::{error::ExtractorSourceError, util::matchmaker::Matchmaker};
|
use crate::{error::ExtractorError, util::matchmaker::Matchmaker};
|
||||||
|
|
||||||
/// Core dependencies for all sevice extractors
|
/// Core dependencies for all sevice extractors
|
||||||
pub struct ExtractorCore {
|
pub struct ExtractorCore {
|
||||||
|
@ -32,7 +32,7 @@ pub struct GetResult<T> {
|
||||||
pub enum GetStatus {
|
pub enum GetStatus {
|
||||||
Fetched,
|
Fetched,
|
||||||
Stored,
|
Stored,
|
||||||
Error(ExtractorSourceError),
|
Error(Box<ExtractorError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for GetStatus {
|
impl PartialEq for GetStatus {
|
||||||
|
|
|
@ -33,6 +33,9 @@ pub(crate) trait ServiceExtractor {
|
||||||
/// Fetch a new track from the streaming service.
|
/// Fetch a new track from the streaming service.
|
||||||
async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError>;
|
async fn fetch_track(&self, src_id: &str) -> Result<i32, ExtractorError>;
|
||||||
|
|
||||||
|
/// Fetch the metadata of a playlist from the streaming service.
|
||||||
|
async fn fetch_playlist_meta(&self, src_id: &str) -> Result<i32, ExtractorError>;
|
||||||
|
|
||||||
/// Update the given playlist from the streaming service,
|
/// Update the given playlist from the streaming service,
|
||||||
/// fetching new tracks if necessary.
|
/// fetching new tracks if necessary.
|
||||||
async fn update_playlist(
|
async fn update_playlist(
|
||||||
|
|
|
@ -2,8 +2,8 @@ use futures::{stream::TryStreamExt, StreamExt};
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rspotify::{
|
use rspotify::{
|
||||||
model::{
|
model::{
|
||||||
AlbumId, ArtistId, FullTrack, Market, PlayableItem, PlaylistId, PublicUser,
|
AlbumId, ArtistId as SpotifyArtistId, FullPlaylist, FullTrack, Image, Market, PlayableItem,
|
||||||
SimplifiedPlaylist, TrackId, UserId,
|
PlaylistId, PublicUser, SimplifiedPlaylist, TrackId, UserId,
|
||||||
},
|
},
|
||||||
prelude::{BaseClient, Id},
|
prelude::{BaseClient, Id},
|
||||||
ClientCredsSpotify,
|
ClientCredsSpotify,
|
||||||
|
@ -11,15 +11,15 @@ use rspotify::{
|
||||||
use std::{collections::HashSet, sync::Arc};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tiraya_db::models::{
|
use tiraya_db::models::{
|
||||||
Artist, MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew, PlaylistType,
|
Artist, ArtistId, EntityType, MusicService, Playlist, PlaylistEntry, PlaylistImgType,
|
||||||
SrcId, SrcIdOwned, Track, User, UserNew, UserType,
|
PlaylistNew, PlaylistType, SrcId, SrcIdOwned, Track, TrackMatchAlbum, TrackMatchMeta, User,
|
||||||
|
UserNew, UserType,
|
||||||
};
|
};
|
||||||
use tiraya_utils::config::CONFIG;
|
use tiraya_utils::config::CONFIG;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ExtractorError,
|
error::ExtractorError,
|
||||||
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
|
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
|
||||||
util::ArtistIdName,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::YouTubeExtractor;
|
use super::YouTubeExtractor;
|
||||||
|
@ -85,7 +85,7 @@ impl SpotifyExtractor {
|
||||||
// will only be called on the initial request of the artist, not on updates
|
// will only be called on the initial request of the artist, not on updates
|
||||||
// (hence no last_update parameter).
|
// (hence no last_update parameter).
|
||||||
self.load_token().await?;
|
self.load_token().await?;
|
||||||
let artist_id = ArtistId::from_id(src_id)?;
|
let artist_id = SpotifyArtistId::from_id(src_id)?;
|
||||||
let mut top_tracks = self
|
let mut top_tracks = self
|
||||||
.sp
|
.sp
|
||||||
.artist_top_tracks(artist_id, Some(self.market))
|
.artist_top_tracks(artist_id, Some(self.market))
|
||||||
|
@ -143,6 +143,7 @@ impl SpotifyExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ExtractorError::NoMatch {
|
Err(ExtractorError::NoMatch {
|
||||||
|
typ: EntityType::Artist,
|
||||||
id: a_id.to_owned(),
|
id: a_id.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -169,6 +170,16 @@ impl SpotifyExtractor {
|
||||||
self.import_track(track).await
|
self.import_track(track).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_playlist_meta(&self, src_id: &str) -> Result<i32, ExtractorError> {
|
||||||
|
self.load_token().await?;
|
||||||
|
let sp_pl_id = PlaylistId::from_id(src_id)?;
|
||||||
|
let playlist = self
|
||||||
|
.sp
|
||||||
|
.playlist(sp_pl_id.clone_static(), None, None)
|
||||||
|
.await?;
|
||||||
|
self.import_playlist(playlist).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_playlist(
|
pub async fn update_playlist(
|
||||||
&self,
|
&self,
|
||||||
src_id: &str,
|
src_id: &str,
|
||||||
|
@ -178,35 +189,17 @@ impl SpotifyExtractor {
|
||||||
let sp_pl_id = PlaylistId::from_id(src_id)?;
|
let sp_pl_id = PlaylistId::from_id(src_id)?;
|
||||||
let playlist = self
|
let playlist = self
|
||||||
.sp
|
.sp
|
||||||
.playlist(sp_pl_id.clone_static(), Some(""), None)
|
.playlist(sp_pl_id.clone_static(), None, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let current_state = LastUpdateState::Version(playlist.snapshot_id);
|
let current_state = LastUpdateState::Version(playlist.snapshot_id.to_owned());
|
||||||
if let Some(last_update) = last_update {
|
if let Some(last_update) = last_update {
|
||||||
if last_update.state.is_current(¤t_state) {
|
if last_update.state.is_current(¤t_state) {
|
||||||
return Ok(GetResult::stored(last_update.id));
|
return Ok(GetResult::stored(last_update.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_url = playlist
|
let playlist_id = self.import_playlist(playlist).await?;
|
||||||
.images
|
|
||||||
.into_iter()
|
|
||||||
.max_by_key(|img| img.height.unwrap_or_default())
|
|
||||||
.map(|img| img.url);
|
|
||||||
let owner_id = self.import_user(playlist.owner).await?;
|
|
||||||
|
|
||||||
let n_playlist = PlaylistNew {
|
|
||||||
src_id,
|
|
||||||
service: MusicService::Spotify,
|
|
||||||
name: &playlist.name,
|
|
||||||
owner_id: Some(owner_id),
|
|
||||||
playlist_type: PlaylistType::Remote,
|
|
||||||
description: playlist.description.as_deref(),
|
|
||||||
image_url: image_url.as_deref(),
|
|
||||||
image_type: Some(PlaylistImgType::Custom).filter(|_| image_url.is_some()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let playlist_id = n_playlist.upsert(&self.core.db).await?;
|
|
||||||
|
|
||||||
let playlist_tracks = self
|
let playlist_tracks = self
|
||||||
.sp
|
.sp
|
||||||
|
@ -292,7 +285,7 @@ impl SpotifyExtractor {
|
||||||
let user = self.sp.user(sp_user_id.clone_static()).await?;
|
let user = self.sp.user(sp_user_id.clone_static()).await?;
|
||||||
let user_id = self.import_user(user).await?;
|
let user_id = self.import_user(user).await?;
|
||||||
|
|
||||||
self.import_playlists(playlists_p1.items).await?;
|
self.import_playlist_items(playlists_p1.items).await?;
|
||||||
|
|
||||||
if playlists_p1.next.is_some() {
|
if playlists_p1.next.is_some() {
|
||||||
let mut offset = PAGE_LIMIT;
|
let mut offset = PAGE_LIMIT;
|
||||||
|
@ -305,7 +298,7 @@ impl SpotifyExtractor {
|
||||||
Some(offset),
|
Some(offset),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
self.import_playlists(playlists.items).await?;
|
self.import_playlist_items(playlists.items).await?;
|
||||||
if playlists.next.is_none() {
|
if playlists.next.is_none() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -319,11 +312,7 @@ impl SpotifyExtractor {
|
||||||
|
|
||||||
async fn import_user(&self, user: PublicUser) -> Result<i32, ExtractorError> {
|
async fn import_user(&self, user: PublicUser) -> Result<i32, ExtractorError> {
|
||||||
let id_owned = SrcIdOwned(user.id.id().to_owned(), MusicService::Spotify);
|
let id_owned = SrcIdOwned(user.id.id().to_owned(), MusicService::Spotify);
|
||||||
let image_url = user
|
let image_url = Self::get_image(user.images);
|
||||||
.images
|
|
||||||
.into_iter()
|
|
||||||
.max_by_key(|img| img.height.unwrap_or_default())
|
|
||||||
.map(|img| img.url);
|
|
||||||
let name = user.display_name.as_deref().unwrap_or(user.id.id());
|
let name = user.display_name.as_deref().unwrap_or(user.id.id());
|
||||||
|
|
||||||
self.core
|
self.core
|
||||||
|
@ -344,7 +333,10 @@ impl SpotifyExtractor {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_playlist(&self, playlist: SimplifiedPlaylist) -> Result<i32, ExtractorError> {
|
async fn import_playlist_item(
|
||||||
|
&self,
|
||||||
|
playlist: SimplifiedPlaylist,
|
||||||
|
) -> Result<i32, ExtractorError> {
|
||||||
if let Some(id) = Playlist::get_id(
|
if let Some(id) = Playlist::get_id(
|
||||||
SrcId(playlist.id.id(), MusicService::Spotify),
|
SrcId(playlist.id.id(), MusicService::Spotify),
|
||||||
&self.core.db,
|
&self.core.db,
|
||||||
|
@ -354,11 +346,7 @@ impl SpotifyExtractor {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_url = playlist
|
let image_url = Self::get_image(playlist.images);
|
||||||
.images
|
|
||||||
.into_iter()
|
|
||||||
.max_by_key(|img| img.height.unwrap_or_default())
|
|
||||||
.map(|img| img.url);
|
|
||||||
let owner_id = self.import_user(playlist.owner).await?;
|
let owner_id = self.import_user(playlist.owner).await?;
|
||||||
|
|
||||||
let playlist_n = PlaylistNew {
|
let playlist_n = PlaylistNew {
|
||||||
|
@ -381,13 +369,13 @@ impl SpotifyExtractor {
|
||||||
Ok(playlist_id)
|
Ok(playlist_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_playlists(
|
async fn import_playlist_items(
|
||||||
&self,
|
&self,
|
||||||
playlists: impl IntoIterator<Item = SimplifiedPlaylist>,
|
playlists: impl IntoIterator<Item = SimplifiedPlaylist>,
|
||||||
) -> Result<(), ExtractorError> {
|
) -> Result<(), ExtractorError> {
|
||||||
futures::stream::iter(playlists.into_iter().map(Ok))
|
futures::stream::iter(playlists.into_iter().map(Ok))
|
||||||
.try_for_each_concurrent(CONFIG.core.db_concurrency, |pl| async {
|
.try_for_each_concurrent(CONFIG.core.db_concurrency, |pl| async {
|
||||||
self.import_playlist(pl).await?;
|
self.import_playlist_item(pl).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -407,24 +395,31 @@ impl SpotifyExtractor {
|
||||||
return Ok(track_id);
|
return Ok(track_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let artists = track
|
|
||||||
.artists
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|a| {
|
|
||||||
a.id.as_ref().map(|a_id| ArtistIdName {
|
|
||||||
id: a_id.id().to_owned(),
|
|
||||||
name: a.name,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
self.yt
|
self.yt
|
||||||
.match_track(
|
.match_track(TrackMatchMeta {
|
||||||
src_id,
|
id: src_id.to_owned(),
|
||||||
&track.name,
|
name: track.name,
|
||||||
&artists,
|
artists: track
|
||||||
track.external_ids.get("isrc").map(|s| s.as_str()),
|
.artists
|
||||||
)
|
.into_iter()
|
||||||
|
.map(|a| ArtistId {
|
||||||
|
id: a
|
||||||
|
.id
|
||||||
|
.map(|id| SrcIdOwned(id.id().to_owned(), MusicService::Spotify)),
|
||||||
|
name: a.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
album: TrackMatchAlbum {
|
||||||
|
id: track
|
||||||
|
.album
|
||||||
|
.id
|
||||||
|
.map(|id| SrcIdOwned(id.id().to_owned(), MusicService::Spotify)),
|
||||||
|
name: track.album.name,
|
||||||
|
image_url: Self::get_image(track.album.images),
|
||||||
|
},
|
||||||
|
isrc: track.external_ids.get("isrc").cloned(),
|
||||||
|
duration: track.duration.num_seconds().try_into().ok(),
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,4 +439,32 @@ impl SpotifyExtractor {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn import_playlist(&self, playlist: FullPlaylist) -> Result<i32, ExtractorError> {
|
||||||
|
let image_url = Self::get_image(playlist.images);
|
||||||
|
let owner_id = self.import_user(playlist.owner).await?;
|
||||||
|
|
||||||
|
let n_playlist = PlaylistNew {
|
||||||
|
src_id: playlist.id.id(),
|
||||||
|
service: MusicService::Spotify,
|
||||||
|
name: &playlist.name,
|
||||||
|
owner_id: Some(owner_id),
|
||||||
|
playlist_type: PlaylistType::Remote,
|
||||||
|
description: playlist.description.as_deref(),
|
||||||
|
image_url: image_url.as_deref(),
|
||||||
|
image_type: Some(PlaylistImgType::Custom).filter(|_| image_url.is_some()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
n_playlist
|
||||||
|
.upsert(&self.core.db)
|
||||||
|
.await
|
||||||
|
.map_err(ExtractorError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image(images: Vec<Image>) -> Option<String> {
|
||||||
|
images
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|img| img.height.unwrap_or_default())
|
||||||
|
.map(|img| img.url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,17 @@ use time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
|
||||||
use tiraya_db::{
|
use tiraya_db::{
|
||||||
error::OptionalRes,
|
error::OptionalRes,
|
||||||
models::{
|
models::{
|
||||||
Album, AlbumNew, AlbumType, AlbumUpdate, Artist, ArtistId, ArtistNew, DatePrecision, Id,
|
Album, AlbumNew, AlbumType, AlbumUpdate, Artist, ArtistId, ArtistNew, DatePrecision,
|
||||||
MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew, PlaylistType, SrcId,
|
EntityType, Id, MusicService, Playlist, PlaylistEntry, PlaylistImgType, PlaylistNew,
|
||||||
SrcIdOwned, Track, TrackNew, TrackUpdate, User, UserNew, UserType,
|
PlaylistType, SrcId, SrcIdOwned, Track, TrackMatchAlbum, TrackMatchData, TrackMatchMeta,
|
||||||
|
TrackNew, TrackUpdate, User, UserNew, UserType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tiraya_utils::config::CONFIG;
|
use tiraya_utils::config::CONFIG;
|
||||||
|
|
||||||
use crate::util::ArtistSrcidName;
|
use crate::util::ArtistSrcidName;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ExtractorError,
|
error::{ExtractorError, MapSrcError},
|
||||||
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
|
model::{ExtractorCore, GetResult, LastUpdateState, SyncLastUpdate},
|
||||||
util::{self, ArtistIdName, ImgFormat},
|
util::{self, ArtistIdName, ImgFormat},
|
||||||
GetStatus,
|
GetStatus,
|
||||||
|
@ -76,7 +77,12 @@ impl YouTubeExtractor {
|
||||||
this_update_state = Some(current_state);
|
this_update_state = Some(current_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let artist = self.rp.query().music_artist(src_id, true).await?;
|
let artist = self
|
||||||
|
.rp
|
||||||
|
.query()
|
||||||
|
.music_artist(src_id, true)
|
||||||
|
.await
|
||||||
|
.mperr(EntityType::Artist, src_id)?;
|
||||||
|
|
||||||
let (top_track_ids, related_artist_ids, related_playlist_ids) = tokio::join!(
|
let (top_track_ids, related_artist_ids, related_playlist_ids) = tokio::join!(
|
||||||
self.import_track_items(
|
self.import_track_items(
|
||||||
|
@ -144,7 +150,12 @@ impl YouTubeExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_album(&self, src_id: &str) -> Result<(i32, bool), ExtractorError> {
|
pub async fn fetch_album(&self, src_id: &str) -> Result<(i32, bool), ExtractorError> {
|
||||||
let album = self.rp.query().music_album(src_id).await?;
|
let album = self
|
||||||
|
.rp
|
||||||
|
.query()
|
||||||
|
.music_album(src_id)
|
||||||
|
.await
|
||||||
|
.mperr(EntityType::Album, src_id)?;
|
||||||
|
|
||||||
let (artists, ul_artists) = self
|
let (artists, ul_artists) = self
|
||||||
.split_yt_artists(album.artists, album.artist_id, &[])
|
.split_yt_artists(album.artists, album.artist_id, &[])
|
||||||
|
@ -229,12 +240,11 @@ impl YouTubeExtractor {
|
||||||
async fn _fetch_track(&self, src_id: &str, with_details: bool) -> Result<i32, ExtractorError> {
|
async fn _fetch_track(&self, src_id: &str, with_details: bool) -> Result<i32, ExtractorError> {
|
||||||
let query = self.rp.query();
|
let query = self.rp.query();
|
||||||
let (track, details) = if with_details {
|
let (track, details) = if with_details {
|
||||||
let (track, details) =
|
tokio::join!(query.music_details(src_id), self.get_track_details(src_id))
|
||||||
tokio::join!(query.music_details(src_id), self.get_track_details(src_id));
|
|
||||||
(track?.track, details)
|
|
||||||
} else {
|
} else {
|
||||||
(query.music_details(src_id).await?.track, None)
|
(query.music_details(src_id).await, None)
|
||||||
};
|
};
|
||||||
|
let track = track.mperr(EntityType::Track, src_id)?.track;
|
||||||
let redirected_id = if track.id != src_id {
|
let redirected_id = if track.id != src_id {
|
||||||
Some(track.id.clone())
|
Some(track.id.clone())
|
||||||
} else {
|
} else {
|
||||||
|
@ -249,8 +259,9 @@ impl YouTubeExtractor {
|
||||||
"track [yt:{src_id}] got redirected to [yt:{redirected_id}], added alias"
|
"track [yt:{src_id}] got redirected to [yt:{redirected_id}], added alias"
|
||||||
);
|
);
|
||||||
Track::add_alias(
|
Track::add_alias(
|
||||||
track_id,
|
|
||||||
SrcId(&redirected_id, MusicService::YouTube),
|
SrcId(&redirected_id, MusicService::YouTube),
|
||||||
|
Some(track_id),
|
||||||
|
None,
|
||||||
&self.core.db,
|
&self.core.db,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -258,6 +269,16 @@ impl YouTubeExtractor {
|
||||||
Ok(track_id)
|
Ok(track_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_playlist_meta(&self, src_id: &str) -> Result<i32, ExtractorError> {
|
||||||
|
let playlist = self
|
||||||
|
.rp
|
||||||
|
.query()
|
||||||
|
.music_playlist(src_id)
|
||||||
|
.await
|
||||||
|
.mperr(EntityType::Playlist, src_id)?;
|
||||||
|
self.import_playlist(&playlist).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_playlist(
|
pub async fn update_playlist(
|
||||||
&self,
|
&self,
|
||||||
src_id: &str,
|
src_id: &str,
|
||||||
|
@ -265,11 +286,12 @@ impl YouTubeExtractor {
|
||||||
) -> Result<GetResult<i32>, ExtractorError> {
|
) -> Result<GetResult<i32>, ExtractorError> {
|
||||||
let query = self.rp.query();
|
let query = self.rp.query();
|
||||||
let (m_playlist, d_playlist) = if src_id.starts_with("RD") {
|
let (m_playlist, d_playlist) = if src_id.starts_with("RD") {
|
||||||
(query.music_playlist(src_id).await?, None)
|
(query.music_playlist(src_id).await, None)
|
||||||
} else {
|
} else {
|
||||||
let (m, d) = tokio::join!(query.music_playlist(src_id), query.playlist(src_id));
|
let (m, d) = tokio::join!(query.music_playlist(src_id), query.playlist(src_id));
|
||||||
(m?, d.ok())
|
(m, d.ok())
|
||||||
};
|
};
|
||||||
|
let m_playlist = m_playlist.mperr(EntityType::Playlist, src_id)?;
|
||||||
|
|
||||||
let current_state = LastUpdateState::Date(
|
let current_state = LastUpdateState::Date(
|
||||||
d_playlist
|
d_playlist
|
||||||
|
@ -283,23 +305,7 @@ impl YouTubeExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_url = util::yt_image_url(&m_playlist.thumbnail, ImgFormat::Cover);
|
let playlist_id = self.import_playlist(&m_playlist).await?;
|
||||||
let owner_id = self
|
|
||||||
.get_playlist_owner(&m_playlist.channel, m_playlist.from_ytm)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let n_playlist = PlaylistNew {
|
|
||||||
src_id,
|
|
||||||
service: MusicService::YouTube,
|
|
||||||
name: &m_playlist.name,
|
|
||||||
owner_id,
|
|
||||||
playlist_type: PlaylistType::Remote,
|
|
||||||
description: m_playlist.description.as_deref(),
|
|
||||||
image_url: image_url.as_deref(),
|
|
||||||
image_type: Some(PlaylistImgType::Custom).filter(|_| image_url.is_some()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let playlist_id = n_playlist.upsert(&self.core.db).await?;
|
|
||||||
|
|
||||||
let mut m_tracks = m_playlist.tracks;
|
let mut m_tracks = m_playlist.tracks;
|
||||||
m_tracks
|
m_tracks
|
||||||
|
@ -351,7 +357,12 @@ impl YouTubeExtractor {
|
||||||
src_id: &str,
|
src_id: &str,
|
||||||
last_update: Option<&SyncLastUpdate>,
|
last_update: Option<&SyncLastUpdate>,
|
||||||
) -> Result<GetResult<i32>, ExtractorError> {
|
) -> Result<GetResult<i32>, ExtractorError> {
|
||||||
let mut channel = self.rp.query().channel_playlists(src_id).await?;
|
let mut channel = self
|
||||||
|
.rp
|
||||||
|
.query()
|
||||||
|
.channel_playlists(src_id)
|
||||||
|
.await
|
||||||
|
.mperr(EntityType::User, src_id)?;
|
||||||
|
|
||||||
// Check if the channel is actually an artist
|
// Check if the channel is actually an artist
|
||||||
if channel.name.ends_with(YTM_CHANNEL_SUFFIX) && channel.content.is_empty() {
|
if channel.name.ends_with(YTM_CHANNEL_SUFFIX) && channel.content.is_empty() {
|
||||||
|
@ -409,39 +420,79 @@ impl YouTubeExtractor {
|
||||||
///
|
///
|
||||||
/// The given `src_id` is added to the database as an alias, so the YT track becomes
|
/// The given `src_id` is added to the database as an alias, so the YT track becomes
|
||||||
/// accessible under the other id.
|
/// accessible under the other id.
|
||||||
pub async fn match_track(
|
pub async fn match_track(&self, meta: TrackMatchMeta) -> Result<i32, ExtractorError> {
|
||||||
&self,
|
let src_id = meta.id.to_owned();
|
||||||
src_id: SrcId<'_>,
|
let match_res = self
|
||||||
name: &str,
|
.get_matching_track(
|
||||||
artists: &[ArtistIdName],
|
src_id.as_srcid(),
|
||||||
isrc: Option<&str>,
|
&meta.name,
|
||||||
) -> Result<i32, ExtractorError> {
|
&meta.artists,
|
||||||
let track_id = self.get_matching_track(src_id, name, artists, isrc).await?;
|
meta.isrc.as_deref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match match_res {
|
||||||
|
Ok((track_id, score)) => {
|
||||||
|
let match_data = TrackMatchData {
|
||||||
|
matchmaker_version: util::matchmaker::VERSION,
|
||||||
|
matchmaker_cfg: CONFIG.extractor.matchmaker_cfg_name.to_owned(),
|
||||||
|
score,
|
||||||
|
original: meta,
|
||||||
|
closest: None,
|
||||||
|
};
|
||||||
|
Track::add_alias(
|
||||||
|
src_id.as_srcid(),
|
||||||
|
Some(track_id),
|
||||||
|
Some(&match_data),
|
||||||
|
&self.core.db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Track::add_alias(track_id, src_id, &self.core.db).await?;
|
if let Some(isrc) = match_data
|
||||||
if let Some(isrc) = isrc.and_then(util::normalize_isrc) {
|
.original
|
||||||
let t_upd = TrackUpdate {
|
.isrc
|
||||||
isrc: Some(Some(&isrc)),
|
.as_deref()
|
||||||
..Default::default()
|
.and_then(util::normalize_isrc)
|
||||||
};
|
{
|
||||||
t_upd.update(Id::Db(track_id), &self.core.db).await?;
|
let t_upd = TrackUpdate {
|
||||||
|
isrc: Some(Some(&isrc)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
t_upd.update(track_id, &self.core.db).await?;
|
||||||
|
}
|
||||||
|
Ok(track_id)
|
||||||
|
}
|
||||||
|
Err(nmt) => {
|
||||||
|
if let Some((closest_track, score)) = nmt {
|
||||||
|
let match_data = TrackMatchData {
|
||||||
|
matchmaker_version: util::matchmaker::VERSION,
|
||||||
|
matchmaker_cfg: CONFIG.extractor.matchmaker_cfg_name.to_owned(),
|
||||||
|
score,
|
||||||
|
original: meta,
|
||||||
|
closest: Some(closest_track),
|
||||||
|
};
|
||||||
|
Track::add_alias(src_id.as_srcid(), None, Some(&match_data), &self.core.db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(ExtractorError::NoMatch {
|
||||||
|
typ: EntityType::Track,
|
||||||
|
id: src_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(track_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_matching_track(
|
async fn get_matching_track(
|
||||||
&self,
|
&self,
|
||||||
src_id: SrcId<'_>,
|
src_id: SrcId<'_>,
|
||||||
name: &str,
|
name: &str,
|
||||||
artists: &[ArtistIdName],
|
artists: &[ArtistId],
|
||||||
isrc: Option<&str>,
|
isrc: Option<&str>,
|
||||||
) -> Result<i32, ExtractorError> {
|
) -> Result<Result<(i32, f32), Option<(TrackMatchMeta, f32)>>, ExtractorError> {
|
||||||
let artist = &artists
|
let artist = if let Some(artist) = artists.first() {
|
||||||
.first()
|
&artist.name
|
||||||
.ok_or_else(|| ExtractorError::NoMatch {
|
} else {
|
||||||
id: src_id.to_owned(),
|
return Ok(Err(None));
|
||||||
})?
|
};
|
||||||
.name;
|
|
||||||
let t1 = self.core.matchmaker.parse_title(name);
|
let t1 = self.core.matchmaker.parse_title(name);
|
||||||
|
|
||||||
// Attempt to find the track by searching YouTube for its ISRC id
|
// Attempt to find the track by searching YouTube for its ISRC id
|
||||||
|
@ -470,12 +521,14 @@ impl YouTubeExtractor {
|
||||||
track
|
track
|
||||||
} else {
|
} else {
|
||||||
let track_id = self._fetch_track(&video.id, false).await?;
|
let track_id = self._fetch_track(&video.id, false).await?;
|
||||||
Track::get(Id::Db(track_id), &self.core.db).await?
|
Track::get(track_id, &self.core.db).await?
|
||||||
};
|
};
|
||||||
|
let album = Album::get(track.album_id, &self.core.db).await?;
|
||||||
|
|
||||||
self.store_matching_artists(artists, src_id.1, &track.artists)
|
if album.album_type != Some(AlbumType::Mv) {
|
||||||
.await?;
|
self.store_matching_artists(artists, &track.artists).await?;
|
||||||
return Ok(track.id);
|
return Ok(Ok((track.id, score)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -513,20 +566,53 @@ impl YouTubeExtractor {
|
||||||
track.name,
|
track.name,
|
||||||
track.id
|
track.id
|
||||||
);
|
);
|
||||||
return self.import_track_item(track, None, &[], None, false).await;
|
Ok(Ok((
|
||||||
|
self.import_track_item(track, None, &[], None, false)
|
||||||
|
.await?,
|
||||||
|
score,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
"could not match track `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})",
|
||||||
|
track.name,
|
||||||
|
track.id
|
||||||
|
);
|
||||||
|
let (album_id, album_name) = if let Some(album) = track.album {
|
||||||
|
(
|
||||||
|
Some(SrcIdOwned(album.id, MusicService::YouTube)),
|
||||||
|
album.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, track.name.clone())
|
||||||
|
};
|
||||||
|
Ok(Err(Some((
|
||||||
|
TrackMatchMeta {
|
||||||
|
id: SrcIdOwned(track.id, MusicService::YouTube),
|
||||||
|
name: track.name,
|
||||||
|
artists: track
|
||||||
|
.artists
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| ArtistId {
|
||||||
|
id: a.id.map(|id| SrcIdOwned(id, MusicService::YouTube)),
|
||||||
|
name: a.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
album: TrackMatchAlbum {
|
||||||
|
id: album_id,
|
||||||
|
name: album_name,
|
||||||
|
image_url: util::yt_image_url(&track.cover, ImgFormat::Cover)
|
||||||
|
.map(|u| u.into_owned()),
|
||||||
|
},
|
||||||
|
isrc: None,
|
||||||
|
duration: track.duration.and_then(|d| d.try_into().ok()),
|
||||||
|
},
|
||||||
|
score,
|
||||||
|
))))
|
||||||
}
|
}
|
||||||
tracing::info!(
|
|
||||||
"could not match track `{name}` [{src_id}] => `{0}` [yt:{1}] (search, {score:.2})",
|
|
||||||
track.name,
|
|
||||||
track.id
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("could not match track `{name}` [{src_id}] (search)");
|
tracing::info!("could not match track `{name}` [{src_id}] (search)");
|
||||||
|
Ok(Err(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ExtractorError::NoMatch {
|
|
||||||
id: src_id.to_owned(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to match the artists from a different music service to the ones of a track
|
/// Try to match the artists from a different music service to the ones of a track
|
||||||
|
@ -535,8 +621,7 @@ impl YouTubeExtractor {
|
||||||
/// Returns the artist id matched to the first given artist
|
/// Returns the artist id matched to the first given artist
|
||||||
async fn store_matching_artists(
|
async fn store_matching_artists(
|
||||||
&self,
|
&self,
|
||||||
artists_to_match: &[ArtistIdName],
|
artists_to_match: &[ArtistId],
|
||||||
service: MusicService,
|
|
||||||
yt_artists: &[ArtistId],
|
yt_artists: &[ArtistId],
|
||||||
) -> Result<(), ExtractorError> {
|
) -> Result<(), ExtractorError> {
|
||||||
let mut yt_artists = yt_artists
|
let mut yt_artists = yt_artists
|
||||||
|
@ -555,57 +640,58 @@ impl YouTubeExtractor {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for atm in artists_to_match {
|
for atm in artists_to_match {
|
||||||
let atm_srcid = SrcIdOwned(atm.id.to_owned(), service);
|
if let Some(atm_srcid) = &atm.id {
|
||||||
// Check if the artist has already been matched
|
// Check if the artist has already been matched
|
||||||
let atm_id_res = self
|
let atm_id_res = self
|
||||||
.core
|
.core
|
||||||
.artist_cache
|
.artist_cache
|
||||||
.get_or_insert_async(&atm_srcid, async {
|
.get_or_insert_async(atm_srcid, async {
|
||||||
Artist::get_id(atm_srcid.as_srcid(), &self.core.db)
|
Artist::get_id(atm_srcid.as_srcid(), &self.core.db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ExtractorError::NoId)
|
.ok_or(ExtractorError::NoId)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
match atm_id_res {
|
match atm_id_res {
|
||||||
Ok(atm_id) => {
|
Ok(atm_id) => {
|
||||||
// Artist is already matched
|
// Artist is already matched
|
||||||
let yt_src_id = Artist::get_src_id(atm_id, &self.core.db)
|
let yt_src_id = Artist::get_src_id(atm_id, &self.core.db)
|
||||||
.await?
|
|
||||||
.ok_or(ExtractorError::NoId)?;
|
|
||||||
if let Some((idx, _)) = yt_artists
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, (a, _))| a.id == yt_src_id)
|
|
||||||
{
|
|
||||||
yt_artists.swap_remove(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(ExtractorError::NoId) => {
|
|
||||||
// Match artist
|
|
||||||
let a1 = self.core.matchmaker.parse_artist(&atm.name);
|
|
||||||
let (ib, score) = yt_artists
|
|
||||||
.iter()
|
|
||||||
.map(|(_, a2)| self.core.matchmaker.match_name(&a1, a2))
|
|
||||||
.enumerate()
|
|
||||||
.max_by(|(_, a), (_, b)| a.total_cmp(b))
|
|
||||||
.unwrap_or_default();
|
|
||||||
if score > 0.8 {
|
|
||||||
let yta = yt_artists.swap_remove(ib).0;
|
|
||||||
let yta_id = Artist::get_id(yta.id, &self.core.db)
|
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ExtractorError::NoId)?;
|
.ok_or(ExtractorError::NoId)?;
|
||||||
Artist::add_alias(yta_id, atm_srcid.as_srcid(), &self.core.db).await?;
|
if let Some((idx, _)) = yt_artists
|
||||||
tracing::info!(
|
.iter()
|
||||||
"matched artist `{}` [{}] => `{}` [{}] ({score:.2})",
|
.enumerate()
|
||||||
yta.name,
|
.find(|(_, (a, _))| a.id == yt_src_id)
|
||||||
yta.id,
|
{
|
||||||
atm.name,
|
yt_artists.swap_remove(idx);
|
||||||
atm_srcid
|
}
|
||||||
);
|
|
||||||
self.core.artist_cache.insert(atm_srcid, yta_id);
|
|
||||||
}
|
}
|
||||||
|
Err(ExtractorError::NoId) => {
|
||||||
|
// Match artist
|
||||||
|
let a1 = self.core.matchmaker.parse_artist(&atm.name);
|
||||||
|
let (ib, score) = yt_artists
|
||||||
|
.iter()
|
||||||
|
.map(|(_, a2)| self.core.matchmaker.match_name(&a1, a2))
|
||||||
|
.enumerate()
|
||||||
|
.max_by(|(_, a), (_, b)| a.total_cmp(b))
|
||||||
|
.unwrap_or_default();
|
||||||
|
if score > 0.8 {
|
||||||
|
let yta = yt_artists.swap_remove(ib).0;
|
||||||
|
let yta_id = Artist::get_id(yta.id, &self.core.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(ExtractorError::NoId)?;
|
||||||
|
Artist::add_alias(yta_id, atm_srcid.as_srcid(), &self.core.db).await?;
|
||||||
|
tracing::info!(
|
||||||
|
"matched artist `{}` [{}] => `{}` [{}] ({score:.2})",
|
||||||
|
yta.name,
|
||||||
|
yta.id,
|
||||||
|
atm.name,
|
||||||
|
atm_srcid
|
||||||
|
);
|
||||||
|
self.core.artist_cache.insert(atm_srcid.to_owned(), yta_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -670,6 +756,7 @@ impl YouTubeExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ExtractorError::NoMatch {
|
Err(ExtractorError::NoMatch {
|
||||||
|
typ: EntityType::Album,
|
||||||
id: src_id.to_owned(),
|
id: src_id.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -678,7 +765,12 @@ impl YouTubeExtractor {
|
||||||
// Internal functions
|
// Internal functions
|
||||||
impl YouTubeExtractor {
|
impl YouTubeExtractor {
|
||||||
async fn artist_update_state(&self, id: &str) -> Result<LastUpdateState, ExtractorError> {
|
async fn artist_update_state(&self, id: &str) -> Result<LastUpdateState, ExtractorError> {
|
||||||
let feed = self.rp.query().channel_rss(id).await?;
|
let feed = self
|
||||||
|
.rp
|
||||||
|
.query()
|
||||||
|
.channel_rss(id)
|
||||||
|
.await
|
||||||
|
.mperr(EntityType::Artist, id)?;
|
||||||
Ok(feed
|
Ok(feed
|
||||||
.videos
|
.videos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -706,7 +798,7 @@ impl YouTubeExtractor {
|
||||||
primary_track: Some(Some(false)),
|
primary_track: Some(Some(false)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
t_upd.update(Id::Db(id), &self.core.db).await?;
|
t_upd.update(id, &self.core.db).await?;
|
||||||
}
|
}
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
@ -996,6 +1088,33 @@ impl YouTubeExtractor {
|
||||||
.map_err(ExtractorError::from)
|
.map_err(ExtractorError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import the metadata of a YouTube Music playlist
|
||||||
|
async fn import_playlist(
|
||||||
|
&self,
|
||||||
|
playlist: &rpmodel::MusicPlaylist,
|
||||||
|
) -> Result<i32, ExtractorError> {
|
||||||
|
let image_url = util::yt_image_url(&playlist.thumbnail, ImgFormat::Cover);
|
||||||
|
let owner_id = self
|
||||||
|
.get_playlist_owner(&playlist.channel, playlist.from_ytm)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let n_playlist = PlaylistNew {
|
||||||
|
src_id: &playlist.id,
|
||||||
|
service: MusicService::YouTube,
|
||||||
|
name: &playlist.name,
|
||||||
|
owner_id,
|
||||||
|
playlist_type: PlaylistType::Remote,
|
||||||
|
description: playlist.description.as_deref(),
|
||||||
|
image_url: image_url.as_deref(),
|
||||||
|
image_type: Some(PlaylistImgType::Custom).filter(|_| image_url.is_some()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
n_playlist
|
||||||
|
.upsert(&self.core.db)
|
||||||
|
.await
|
||||||
|
.map_err(ExtractorError::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert track/album artists scraped from YouTube into the format used by Tiraya
|
/// Convert track/album artists scraped from YouTube into the format used by Tiraya
|
||||||
///
|
///
|
||||||
/// YouTube Music's metadata is incomplete and there are a lot of tracks with
|
/// YouTube Music's metadata is incomplete and there are a lot of tracks with
|
||||||
|
@ -1148,7 +1267,7 @@ impl YouTubeExtractor {
|
||||||
release_date_precision: Some(Some(release_date.1)),
|
release_date_precision: Some(Some(release_date.1)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
b_upd.update(Id::Db(album_id), &self.core.db).await?;
|
b_upd.update(album_id, &self.core.db).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1195,18 +1314,27 @@ mod tests {
|
||||||
async fn match_track() {
|
async fn match_track() {
|
||||||
let xtr = YouTubeExtractor::new(ExtractorCore::new(pool.clone()).into()).unwrap();
|
let xtr = YouTubeExtractor::new(ExtractorCore::new(pool.clone()).into()).unwrap();
|
||||||
let track_id = xtr
|
let track_id = xtr
|
||||||
.match_track(
|
.match_track(TrackMatchMeta {
|
||||||
SrcId("5awNIWVrh2ISfvPd5IUZNh", MusicService::Spotify),
|
id: SrcIdOwned("5awNIWVrh2ISfvPd5IUZNh".to_owned(), MusicService::Spotify),
|
||||||
"PTT (Paint The Town)",
|
name: "PTT (Paint The Town)".to_owned(),
|
||||||
&[ArtistIdName {
|
artists: vec![ArtistId {
|
||||||
id: "52zMTJCKluDlFwMQWmccY7".to_owned(),
|
id: Some(SrcIdOwned(
|
||||||
|
"52zMTJCKluDlFwMQWmccY7".to_owned(),
|
||||||
|
MusicService::Spotify,
|
||||||
|
)),
|
||||||
name: "LOONA".to_owned(),
|
name: "LOONA".to_owned(),
|
||||||
}],
|
}],
|
||||||
Some("KRA382152284"),
|
album: TrackMatchAlbum {
|
||||||
)
|
id: None,
|
||||||
|
name: "Paint The Town".to_owned(),
|
||||||
|
image_url: None,
|
||||||
|
},
|
||||||
|
isrc: Some("KRA382152284".to_owned()),
|
||||||
|
duration: None,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let track = Track::get(Id::Db(track_id), &pool).await.unwrap();
|
let track = Track::get(track_id, &pool).await.unwrap();
|
||||||
insta::assert_ron_snapshot!(track, {
|
insta::assert_ron_snapshot!(track, {
|
||||||
".id" => "[id]",
|
".id" => "[id]",
|
||||||
".created_at" => "[date]",
|
".created_at" => "[date]",
|
||||||
|
@ -1229,7 +1357,7 @@ mod tests {
|
||||||
album_id: 1,
|
album_id: 1,
|
||||||
album_pos: None,
|
album_pos: None,
|
||||||
isrc: Some("KRA382152284"),
|
isrc: Some("KRA382152284"),
|
||||||
description: Some("Provided to YouTube by Kakao Entertainment\n\nPTT (Paint The Town) (PTT (Paint The Town)) · LOONA\n\n[&]\n\n℗ 2021 BlockBerry Creative,under license to Kakao Entertainment\n\nReleased on: 2021-06-28\n\nAuthor: Ryan S. Jhun\nAuthor: Hanif Hitmanic Sabzevari\nAuthor: Dennis DeKo Kordnejad\nAuthor: YOUHA\nComposer: Ryan S. Jhun\nComposer: Hanif Hitmanic Sabzevari\nComposer: Dennis DeKo Kordnejad\nComposer: YOUHA\nArranger: Ryan S. Jhun\nArranger: Hanif Hitmanic Sabzevari\nArranger: Dennis DeKo Kordnejad\n\nAuto-generated by YouTube."),
|
description: None,
|
||||||
created_at: "[date]",
|
created_at: "[date]",
|
||||||
updated_at: "[date]",
|
updated_at: "[date]",
|
||||||
primary_track: None,
|
primary_track: None,
|
||||||
|
|
|
@ -19,6 +19,10 @@ static SEPARATORS: phf::Set<char> =
|
||||||
|
|
||||||
static SEPARATORS_NOSPACE: phf::Set<char> = phf::phf_set!('|', 'ᅵ', '-');
|
static SEPARATORS_NOSPACE: phf::Set<char> = phf::phf_set!('|', 'ᅵ', '-');
|
||||||
|
|
||||||
|
/// The version of the matchmaker. It is incremented with every Tiraya release which
|
||||||
|
/// includes changes to the matchmaker logic.
|
||||||
|
pub const VERSION: u16 = 1;
|
||||||
|
|
||||||
pub struct Matchmaker {
|
pub struct Matchmaker {
|
||||||
kw_matcher: AhoCorasick,
|
kw_matcher: AhoCorasick,
|
||||||
/// List of numbers indicating the first index of a specific keyword
|
/// List of numbers indicating the first index of a specific keyword
|
||||||
|
@ -60,7 +64,10 @@ impl Matchmaker {
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
kw_matcher: AhoCorasick::new(patterns).unwrap(),
|
kw_matcher: AhoCorasick::builder()
|
||||||
|
.match_kind(aho_corasick::MatchKind::LeftmostLongest)
|
||||||
|
.build(patterns)
|
||||||
|
.unwrap(),
|
||||||
kw_positions: positions,
|
kw_positions: positions,
|
||||||
match_thr,
|
match_thr,
|
||||||
}
|
}
|
||||||
|
@ -346,7 +353,10 @@ mod tests {
|
||||||
|
|
||||||
static KEYWORDS: Lazy<BTreeMap<&str, Vec<&str>>> = Lazy::new(|| {
|
static KEYWORDS: Lazy<BTreeMap<&str, Vec<&str>>> = Lazy::new(|| {
|
||||||
let mut keywords = BTreeMap::new();
|
let mut keywords = BTreeMap::new();
|
||||||
keywords.insert("acoustic", vec!["acoustic", "akustik", "unplugged"]);
|
keywords.insert(
|
||||||
|
"acoustic",
|
||||||
|
vec!["acoustic", "akustik", "unplugged", "piano", "pianos"],
|
||||||
|
);
|
||||||
keywords.insert(
|
keywords.insert(
|
||||||
"instrumental",
|
"instrumental",
|
||||||
vec![
|
vec![
|
||||||
|
@ -372,12 +382,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn get_kw_type() {
|
fn get_kw_type() {
|
||||||
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
||||||
assert_eq!(mm.kw_positions, &[0, 3, 11]);
|
assert_eq!(mm.kw_positions, &[0, 5, 13]);
|
||||||
|
|
||||||
assert_eq!(mm.get_kw_type(0), TYPE_ACOUSTIC);
|
assert_eq!(mm.get_kw_type(0), TYPE_ACOUSTIC);
|
||||||
assert_eq!(mm.get_kw_type(1), TYPE_ACOUSTIC);
|
assert_eq!(mm.get_kw_type(1), TYPE_ACOUSTIC);
|
||||||
assert_eq!(mm.get_kw_type(3), TYPE_INSTRUMENTAL);
|
assert_eq!(mm.get_kw_type(5), TYPE_INSTRUMENTAL);
|
||||||
assert_eq!(mm.get_kw_type(11), TYPE_LIVE);
|
assert_eq!(mm.get_kw_type(13), TYPE_LIVE);
|
||||||
assert_eq!(mm.get_kw_type(100), TYPE_LIVE);
|
assert_eq!(mm.get_kw_type(100), TYPE_LIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,6 +531,7 @@ mod tests {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(r#"INNOCENCE(Live at 渋谷公会堂)"#, r#"INNOCENCE"#)]
|
#[case(r#"INNOCENCE(Live at 渋谷公会堂)"#, r#"INNOCENCE"#)]
|
||||||
#[case(r#"Zu dir"#, r#"Zruck zu dir (Hallo Klaus)"#)]
|
#[case(r#"Zu dir"#, r#"Zruck zu dir (Hallo Klaus)"#)]
|
||||||
|
#[case(r#"Jump!"#, r#"Jump! (For Two Pianos)"#)]
|
||||||
fn no_match_titles(#[case] a: &str, #[case] b: &str) {
|
fn no_match_titles(#[case] a: &str, #[case] b: &str) {
|
||||||
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
||||||
let parsed_a = mm.parse_title(a);
|
let parsed_a = mm.parse_title(a);
|
||||||
|
@ -534,7 +545,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(r#"Max-Antoine"#, r#"Max Antoine Meisters"#)]
|
#[case(r#"Max-Antoine"#, r#"Max-Antoine Meisters"#)]
|
||||||
#[case(r#"DJ Gollum"#, r#"DJ Gollum feat. Akustikrausch"#)]
|
#[case(r#"DJ Gollum"#, r#"DJ Gollum feat. Akustikrausch"#)]
|
||||||
fn match_artists(#[case] a: &str, #[case] b: &str) {
|
fn match_artists(#[case] a: &str, #[case] b: &str) {
|
||||||
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
let mm = Matchmaker::new(KEYWORDS.values(), MATCH_THR);
|
||||||
|
|
|
@ -3,7 +3,6 @@ pub mod matchmaker;
|
||||||
use std::{borrow::Cow, hash::Hasher};
|
use std::{borrow::Cow, hash::Hasher};
|
||||||
|
|
||||||
use hex_literal::hex;
|
use hex_literal::hex;
|
||||||
use http::StatusCode;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustypipe::model as rpmodel;
|
use rustypipe::model as rpmodel;
|
||||||
|
@ -11,7 +10,7 @@ use siphasher::sip128::{Hasher128, SipHasher};
|
||||||
use time::{Date, Duration, OffsetDateTime};
|
use time::{Date, Duration, OffsetDateTime};
|
||||||
use tiraya_db::models::{AlbumType, DatePrecision, SrcId, SyncData, SyncError};
|
use tiraya_db::models::{AlbumType, DatePrecision, SrcId, SyncData, SyncError};
|
||||||
|
|
||||||
use crate::error::ExtractorSourceError;
|
use crate::error::{ExtractorError, ExtractorSourceError};
|
||||||
|
|
||||||
pub struct ArtistSrcidName<'a> {
|
pub struct ArtistSrcidName<'a> {
|
||||||
pub id: SrcId<'a>,
|
pub id: SrcId<'a>,
|
||||||
|
@ -59,36 +58,24 @@ pub fn yt_image_url(images: &[rpmodel::Thumbnail], format: ImgFormat) -> Option<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src_error_to_sync_data(error: &ExtractorSourceError) -> Option<SyncData> {
|
/// Convert an extractor error to a SyncData object if the error should be stored in the database
|
||||||
|
pub fn error_to_sync_data(error: &ExtractorError) -> Option<SyncData> {
|
||||||
match error {
|
match error {
|
||||||
ExtractorSourceError::YouTube(error) => match error {
|
ExtractorError::Source(error) => match error {
|
||||||
rustypipe::error::Error::Http(_) => None,
|
ExtractorSourceError::YouTube(error) => match error {
|
||||||
rustypipe::error::Error::Extraction(
|
rustypipe::error::Error::Extraction(_) => Some(SyncData::Error {
|
||||||
rustypipe::error::ExtractionError::NotFound { .. }
|
typ: SyncError::Other,
|
||||||
| rustypipe::error::ExtractionError::VideoUnavailable { .. },
|
msg: error.to_string(),
|
||||||
) => Some(SyncData::Error {
|
}),
|
||||||
typ: SyncError::NotFound,
|
_ => None,
|
||||||
msg: error.to_string(),
|
|
||||||
}),
|
|
||||||
rustypipe::error::Error::Extraction(error) => Some(SyncData::Error {
|
|
||||||
typ: SyncError::Other,
|
|
||||||
msg: error.to_string(),
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
ExtractorSourceError::Spotify(error) => match error {
|
|
||||||
rspotify::ClientError::Http(error) => match error.as_ref() {
|
|
||||||
rspotify::http::HttpError::Client(_) => None,
|
|
||||||
rspotify::http::HttpError::StatusCode(status) => match status.status() {
|
|
||||||
StatusCode::NOT_FOUND => Some(SyncData::Error {
|
|
||||||
typ: SyncError::NotFound,
|
|
||||||
msg: String::new(),
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
_ => None,
|
ExtractorSourceError::Spotify(_) => None,
|
||||||
},
|
},
|
||||||
|
ExtractorError::Unavailable { msg, .. } => Some(SyncData::Error {
|
||||||
|
typ: SyncError::Unavailable,
|
||||||
|
msg: msg.to_string(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ enum Commands {
|
||||||
Album { id: String },
|
Album { id: String },
|
||||||
Track { id: String },
|
Track { id: String },
|
||||||
Playlist { id: String },
|
Playlist { id: String },
|
||||||
|
PlaylistMeta { id: String },
|
||||||
User { id: String },
|
User { id: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +78,11 @@ async fn run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Commands::PlaylistMeta { id } => {
|
||||||
|
let src_id = SrcIdOwned::from_str(&id).unwrap();
|
||||||
|
let playlist = ext.get_playlist_meta(src_id.as_srcid()).await.unwrap().c;
|
||||||
|
dbg!(playlist);
|
||||||
|
}
|
||||||
Commands::User { id } => {
|
Commands::User { id } => {
|
||||||
let src_id = SrcIdOwned::from_str(&id).unwrap();
|
let src_id = SrcIdOwned::from_str(&id).unwrap();
|
||||||
let user = ext.get_user(src_id.as_srcid()).await.unwrap().c;
|
let user = ext.get_user(src_id.as_srcid()).await.unwrap().c;
|
||||||
|
|
|
@ -13,6 +13,7 @@ once_cell.workspace = true
|
||||||
path-absolutize.workspace = true
|
path-absolutize.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smart-default.workspace = true
|
smart-default.workspace = true
|
||||||
|
time.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -70,6 +70,12 @@ pub struct ConfigExtractor {
|
||||||
/// Must be between 0 (no match) and 1 (perfect match).
|
/// Must be between 0 (no match) and 1 (perfect match).
|
||||||
#[default(0.5)]
|
#[default(0.5)]
|
||||||
pub match_threshold: f32,
|
pub match_threshold: f32,
|
||||||
|
/// Give your matchmaker configuration a name
|
||||||
|
///
|
||||||
|
/// The name is stored alongside the matches. This allows you to change the
|
||||||
|
/// configuration and redo the matches already stored in the database.
|
||||||
|
#[default("default")]
|
||||||
|
pub matchmaker_cfg_name: String,
|
||||||
|
|
||||||
pub youtube: ConfigExtractorYouTube,
|
pub youtube: ConfigExtractorYouTube,
|
||||||
pub spotify: ConfigExtractorSpotify,
|
pub spotify: ConfigExtractorSpotify,
|
||||||
|
|
|
@ -11,12 +11,17 @@ Config(
|
||||||
extractor: ConfigExtractor(
|
extractor: ConfigExtractor(
|
||||||
artist_stale_h: 24,
|
artist_stale_h: 24,
|
||||||
playlist_stale_h: 24,
|
playlist_stale_h: 24,
|
||||||
|
user_stale_h: 24,
|
||||||
|
playlist_item_limit: 2000,
|
||||||
|
playlist_item_limit_matchmaker: 500,
|
||||||
|
user_playlist_limit: 200,
|
||||||
data_dir: "[path]",
|
data_dir: "[path]",
|
||||||
track_type_keywords: "[track_type_keywords]",
|
track_type_keywords: "[track_type_keywords]",
|
||||||
artist_playlist_excluded_types: [
|
artist_playlist_excluded_types: [
|
||||||
"inst",
|
"inst",
|
||||||
],
|
],
|
||||||
match_threshold: 0.5,
|
match_threshold: 0.5,
|
||||||
|
matchmaker_cfg_name: "default-1",
|
||||||
youtube: ConfigExtractorYouTube(
|
youtube: ConfigExtractorYouTube(
|
||||||
language: "en",
|
language: "en",
|
||||||
country: "US",
|
country: "US",
|
||||||
|
@ -27,7 +32,7 @@ Config(
|
||||||
enable: true,
|
enable: true,
|
||||||
client_id: "abc123",
|
client_id: "abc123",
|
||||||
client_secret: "supersecret",
|
client_secret: "supersecret",
|
||||||
market: None,
|
market: "US",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,6 +28,10 @@ artist_playlist_excluded_types = ["inst"]
|
||||||
# Threshold value for the matchmaker
|
# Threshold value for the matchmaker
|
||||||
# Must be between 0 (no match) and 1 (perfect match).
|
# Must be between 0 (no match) and 1 (perfect match).
|
||||||
match_threshold = 0.5
|
match_threshold = 0.5
|
||||||
|
# Give your matchmaker configuration a name
|
||||||
|
# The name is stored alongside the matches. This allows you to change the
|
||||||
|
# configuration and redo the matches already stored in the database.
|
||||||
|
matchmaker_cfg_name = "default-1"
|
||||||
|
|
||||||
# Track types are used by the matchmaker. Tracks from different types will not match.
|
# Track types are used by the matchmaker. Tracks from different types will not match.
|
||||||
# The keywords must be contained in the title of the track, excluding the first word.
|
# The keywords must be contained in the title of the track, excluding the first word.
|
||||||
|
@ -45,7 +49,8 @@ inst = [
|
||||||
"instr.",
|
"instr.",
|
||||||
]
|
]
|
||||||
live = ["live"]
|
live = ["live"]
|
||||||
acoustic = ["acoustic", "akustik", "unplugged"]
|
acoustic = ["acoustic", "akustik", "unplugged", "piano", "pianos"]
|
||||||
|
nightcore = ["nightcore"]
|
||||||
|
|
||||||
# Configuration for the individual streaming services
|
# Configuration for the individual streaming services
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue