A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz

save dropbox and googledrive directories into database

+307 -77
+48
crates/dropbox/src/repo/dropbox_directory.rs
··· 1 + use sqlx::{Pool, Postgres}; 2 + use crate::{types::file::Entry, xata::dropbox_diretory::DropboxDirectory}; 3 + 4 + pub async fn create_dropbox_directory( 5 + pool: &Pool<Postgres>, 6 + file: &Entry, 7 + dropbox_id: &str, 8 + parent_dir: &str, 9 + ) -> Result<(), sqlx::Error> { 10 + let results: Vec<DropboxDirectory> = sqlx::query_as( 11 + r#" 12 + SELECT * 13 + FROM dropbox_directory 14 + WHERE dropbox_id = $1 15 + AND path = $2 16 + LIMIT 1 17 + "#, 18 + ) 19 + .bind(dropbox_id) 20 + .bind(parent_dir) 21 + .fetch_all(pool) 22 + .await?; 23 + 24 + let parent_id = results.first().map(|d| d.xata_id.clone()); 25 + 26 + sqlx::query( 27 + r#" 28 + INSERT INTO dropbox_directory ( 29 + dropbox_id, 30 + name, 31 + path, 32 + file_id, 33 + parent_id 34 + ) 35 + VALUES ($1, $2, $3, $4, $5) 36 + ON CONFLICT DO NOTHING 37 + "#, 38 + ) 39 + .bind(dropbox_id) 40 + .bind(&file.name) 41 + .bind(&file.path_display) 42 + .bind(&file.id) 43 + .bind(parent_id) 44 + .execute(pool) 45 + .await?; 46 + 47 + Ok(()) 48 + }
+36 -2
crates/dropbox/src/repo/dropbox_path.rs
··· 1 1 use sqlx::{Pool, Postgres}; 2 2 3 3 use crate::types::file::Entry; 4 + use crate::xata::dropbox_diretory::DropboxDirectory; 4 5 use crate::xata::track::Track; 5 6 6 7 pub async fn create_dropbox_path( ··· 8 9 file: &Entry, 9 10 track: &Track, 10 11 dropbox_id: &str, 12 + parent_dir: Option<&str>, 11 13 ) -> Result<(), sqlx::Error> { 14 + let results: Vec<DropboxDirectory> = sqlx::query_as( 15 + r#" 16 + SELECT * 17 + FROM dropbox_directory 18 + WHERE dropbox_id = $1 19 + AND path = $2 20 + LIMIT 1 21 + "#, 22 + ) 23 + .bind(dropbox_id) 24 + .bind(parent_dir) 25 + .fetch_all(pool) 26 + .await?; 27 + 28 + let parent_dir = match parent_dir { 29 + Some(_) => results.first().map(|d| d.clone().xata_id), 30 + None => None, 31 + }; 32 + 12 33 sqlx::query( 13 34 r#" 14 - INSERT INTO dropbox_paths (dropbox_id, path, file_id, track_id, name) 15 - VALUES ($1, $2, $3, $4, $5) 35 + INSERT INTO dropbox_paths (dropbox_id, path, file_id, track_id, name, directory_id) 36 + VALUES ($1, $2, $3, $4, $5, $6) 16 37 ON CONFLICT DO NOTHING 17 38 "#, 18 39 ) ··· 21 42 .bind(&file.id) 22 43 .bind(&track.xata_id) 23 44 .bind(&file.name) 45 + .bind(&parent_dir) 46 + .execute(pool) 47 + .await?; 48 + 49 + sqlx::query( 50 + r#" 51 + UPDATE dropbox_paths 52 + SET directory_id = $1 53 + WHERE file_id = $2 54 + "#, 55 + ) 56 + .bind(&parent_dir) 57 + .bind(&file.id) 24 58 .execute(pool) 25 59 .await?; 26 60
+1
crates/dropbox/src/repo/mod.rs
··· 1 + pub mod dropbox_directory; 1 2 pub mod dropbox_path; 2 3 pub mod dropbox_token; 3 4 pub mod track;
+43 -36
crates/dropbox/src/scan.rs
··· 22 22 consts::AUDIO_EXTENSIONS, 23 23 crypto::decrypt_aes_256_ctr, 24 24 repo::{ 25 - dropbox_path::create_dropbox_path, 26 - dropbox_token::{find_dropbox_refresh_token, find_dropbox_refresh_tokens}, 27 - track::get_track_by_hash, 25 + dropbox_directory::create_dropbox_directory, dropbox_path::create_dropbox_path, dropbox_token::{find_dropbox_refresh_token, find_dropbox_refresh_tokens}, track::get_track_by_hash 28 26 }, 29 27 token::generate_token, 30 28 types::file::{Entry, EntryList}, ··· 98 96 99 97 if entry.tag.clone().unwrap().as_str() == "folder" { 100 98 println!("Scanning folder: {}", path.bright_green()); 99 + 100 + create_dropbox_directory(&pool, &entry, &dropbox_id, &path).await?; 101 + 102 + // TODO: publish folder metadata to nats 101 103 102 104 let mut entries: Vec<Entry> = Vec::new(); 103 105 ··· 281 283 match track { 282 284 Some(track) => { 283 285 println!("Track exists: {}", title.bright_green()); 284 - let status = create_dropbox_path(&pool, &entry, &track, &dropbox_id).await; 286 + let status = create_dropbox_path(&pool, &entry, &track, &dropbox_id, Some(&path)).await; 285 287 println!("status: {:?}", status); 288 + 289 + // TODO: publish file metadata to nats 286 290 } 287 291 None => { 288 292 println!("Creating track: {}", title.bright_green()); ··· 291 295 let client = Client::new(); 292 296 const URL: &str = "https://api.rocksky.app/tracks"; 293 297 let response = client 294 - .post(URL) 295 - .header("Authorization", format!("Bearer {}", access_token)) 296 - .json(&serde_json::json!({ 297 - "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 298 - "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 299 - "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 300 - "albumArtist": match tag.get_string(&lofty::tag::ItemKey::AlbumArtist) { 301 - Some(album_artist) => Some(album_artist), 302 - None => Some(tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default()), 303 - }, 304 - "duration": duration, 305 - "trackNumber": tag.track(), 306 - "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 307 - true => Some(date), 308 - false => None, 309 - }), 310 - "year": tag.year(), 311 - "discNumber": tag.disk().map(|disc| match disc { 312 - 0 => Some(1), 313 - _ => Some(disc), 314 - }).unwrap_or(Some(1)), 315 - "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 316 - "albumArt": match album_art{ 317 - Some(album_art) => Some(format!("https://cdn.rocksky.app/covers/{}", album_art)), 318 - None => None 319 - }, 320 - "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 321 - "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 322 - })) 323 - .send() 324 - .await?; 298 + .post(URL) 299 + .header("Authorization", format!("Bearer {}", access_token)) 300 + .json(&serde_json::json!({ 301 + "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 302 + "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 303 + "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 304 + "albumArtist": match tag.get_string(&lofty::tag::ItemKey::AlbumArtist) { 305 + Some(album_artist) => Some(album_artist), 306 + None => Some(tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default()), 307 + }, 308 + "duration": duration, 309 + "trackNumber": tag.track(), 310 + "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 311 + true => Some(date), 312 + false => None, 313 + }), 314 + "year": tag.year(), 315 + "discNumber": tag.disk().map(|disc| match disc { 316 + 0 => Some(1), 317 + _ => Some(disc), 318 + }).unwrap_or(Some(1)), 319 + "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 320 + "albumArt": match album_art{ 321 + Some(album_art) => Some(format!("https://cdn.rocksky.app/covers/{}", album_art)), 322 + None => None 323 + }, 324 + "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 325 + "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 326 + })) 327 + .send() 328 + .await?; 325 329 println!("Track Saved: {} {}", title, response.status()); 326 330 tokio::time::sleep(std::time::Duration::from_secs(3)).await; 327 331 328 332 let track = get_track_by_hash(&pool, &hash).await?; 329 333 if let Some(track) = track { 330 - create_dropbox_path(&pool, &entry, &track, &dropbox_id).await?; 334 + create_dropbox_path(&pool, &entry, &track, &dropbox_id, Some(&path)).await?; 335 + 336 + // TODO: publish file metadata to nats 337 + 331 338 return Ok(()); 332 339 } 333 340
+15
crates/dropbox/src/xata/dropbox_diretory.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] 5 + pub struct DropboxDirectory { 6 + pub xata_id: String, 7 + pub name: String, 8 + pub path: String, 9 + pub parent_id: Option<String>, 10 + pub xata_version: i32, 11 + #[serde(with = "chrono::serde::ts_seconds")] 12 + pub xata_createdat: DateTime<Utc>, 13 + #[serde(with = "chrono::serde::ts_seconds")] 14 + pub xata_updatedat: DateTime<Utc>, 15 + }
+3
crates/dropbox/src/xata/dropbox_path.rs
··· 5 5 pub struct DropboxPath { 6 6 pub xata_id: String, 7 7 pub dropbox_id: String, 8 + pub path: String, 9 + pub name: String, 10 + pub directory_id: Option<String>, 8 11 pub track_id: String, 9 12 pub xata_version: i32, 10 13 #[serde(with = "chrono::serde::ts_seconds")]
+1
crates/dropbox/src/xata/mod.rs
··· 1 1 pub mod dropbox; 2 + pub mod dropbox_diretory; 2 3 pub mod dropbox_path; 3 4 pub mod dropbox_token; 4 5 pub mod track;
+66
crates/googledrive/src/repo/google_drive_directory.rs
··· 1 + use sqlx::{Pool, Postgres}; 2 + use crate::{types::file::File, xata::google_drive_directory::GoogleDriveDirectory}; 3 + 4 + pub async fn create_google_drive_directory( 5 + pool: &Pool<Postgres>, 6 + file: &File, 7 + google_drive_id: &str, 8 + parent_drive_file_id: Option<&str>, 9 + ) -> Result<(), sqlx::Error> { 10 + let parent = if let Some(parent_id) = parent_drive_file_id { 11 + let results: Vec<GoogleDriveDirectory> = sqlx::query_as( 12 + r#" 13 + SELECT * 14 + FROM google_drive_directory 15 + WHERE google_drive_id = $1 16 + AND file_id = $2 17 + LIMIT 1 18 + "# 19 + ) 20 + .bind(google_drive_id) 21 + .bind(parent_id) 22 + .fetch_all(pool) 23 + .await?; 24 + if results.is_empty() { 25 + None 26 + } else { 27 + Some(results[0].clone()) 28 + } 29 + } else { 30 + None 31 + }; 32 + 33 + let (path, parent_id) = match parent { 34 + Some(p) => ( 35 + format!("{}/{}", p.path.trim_end_matches('/'), file.name), 36 + Some(p.xata_id), 37 + ), 38 + None => ( 39 + format!("/{}", file.name), 40 + None, 41 + ), 42 + }; 43 + 44 + sqlx::query( 45 + r#" 46 + INSERT INTO google_drive_directory ( 47 + google_drive_id, 48 + name, 49 + path, 50 + file_id, 51 + parent_id 52 + ) 53 + VALUES ($1, $2, $3, $4, $5) 54 + ON CONFLICT DO NOTHING 55 + "# 56 + ) 57 + .bind(google_drive_id) 58 + .bind(&file.name) 59 + .bind(&path) 60 + .bind(&file.id) 61 + .bind(parent_id) 62 + .execute(pool) 63 + .await?; 64 + 65 + Ok(()) 66 + }
+33 -3
crates/googledrive/src/repo/google_drive_path.rs
··· 1 1 use sqlx::{Pool, Postgres}; 2 2 3 - use crate::{types::file::File, xata::track::Track}; 3 + use crate::{types::file::File, xata::{google_drive_directory::GoogleDriveDirectory, track::Track}}; 4 4 5 5 pub async fn create_google_drive_path( 6 6 pool: &Pool<Postgres>, 7 7 file: &File, 8 8 track: &Track, 9 9 google_drive_id: &str, 10 + parent_dir: &str, 10 11 ) -> Result<(), sqlx::Error> { 12 + let parent_dir: Vec<GoogleDriveDirectory> = sqlx::query_as( 13 + r#" 14 + SELECT * 15 + FROM google_drive_directory 16 + WHERE google_drive_id = $1 17 + AND file_id = $2 18 + LIMIT 1 19 + "#, 20 + ) 21 + .bind(google_drive_id) 22 + .bind(parent_dir) 23 + .fetch_all(pool) 24 + .await?; 25 + 26 + let parent_dir = parent_dir.first().map(|d| d.clone().xata_id); 27 + 11 28 let result = sqlx::query( 12 29 r#" 13 - INSERT INTO google_drive_paths (google_drive_id, file_id, track_id, name) 14 - VALUES ($1, $2, $3, $4) 30 + INSERT INTO google_drive_paths (google_drive_id, file_id, track_id, name, directory_id) 31 + VALUES ($1, $2, $3, $4, $5) 15 32 ON CONFLICT DO NOTHING 16 33 "#, 17 34 ) ··· 19 36 .bind(&file.id) 20 37 .bind(&track.xata_id) 21 38 .bind(&file.name) 39 + .bind(&parent_dir) 22 40 .execute(pool) 23 41 .await?; 24 42 25 43 println!("{:?}", result); 44 + 45 + sqlx::query( 46 + r#" 47 + UPDATE google_drive_paths 48 + SET directory_id = $1 49 + WHERE file_id = $2 50 + "#, 51 + ) 52 + .bind(&parent_dir) 53 + .bind(&file.id) 54 + .execute(pool) 55 + .await?; 26 56 27 57 Ok(()) 28 58 }
+1
crates/googledrive/src/repo/mod.rs
··· 1 + pub mod google_drive_directory; 1 2 pub mod google_drive_path; 2 3 pub mod google_drive_token; 3 4 pub mod track;
+41 -36
crates/googledrive/src/scan.rs
··· 21 21 consts::AUDIO_EXTENSIONS, 22 22 crypto::decrypt_aes_256_ctr, 23 23 repo::{ 24 - google_drive_path::create_google_drive_path, 25 - google_drive_token::{find_google_drive_refresh_token, find_google_drive_refresh_tokens}, 26 - track::get_track_by_hash, 24 + google_drive_directory::create_google_drive_directory, google_drive_path::create_google_drive_path, google_drive_token::{find_google_drive_refresh_token, find_google_drive_refresh_tokens}, track::get_track_by_hash 27 25 }, 28 26 token::generate_token, 29 27 types::file::{File, FileList}, ··· 101 99 102 100 if file.mime_type == "application/vnd.google-apps.folder" { 103 101 println!("Scanning folder: {}", file.name.bright_green()); 102 + 103 + create_google_drive_directory(&pool, &file, &google_drive_id, Some(&file_id)).await?; 104 + 105 + // TODO: publish folder metadata to nats 104 106 105 107 let mut page_token: Option<String> = None; 106 108 let mut files: Vec<File> = Vec::new(); ··· 291 293 Some(track) => { 292 294 println!("Track exists: {}", title.bright_green()); 293 295 let status = 294 - create_google_drive_path(&pool, &file, &track, &google_drive_id).await?; 296 + create_google_drive_path(&pool, &file, &track, &google_drive_id, &file_id).await?; 295 297 296 298 println!("status: {:?}", status); 299 + // TODO: publish file metadata to nats 297 300 } 298 301 None => { 299 302 println!("Creating track: {}", title.bright_green()); ··· 304 307 let client = Client::new(); 305 308 const URL: &str = "https://api.rocksky.app/tracks"; 306 309 let response = client 307 - .post(URL) 308 - .header("Authorization", format!("Bearer {}", access_token)) 309 - .json(&serde_json::json!({ 310 - "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 311 - "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 312 - "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 313 - "albumArtist": match tag.get_string(&lofty::tag::ItemKey::AlbumArtist) { 314 - Some(album_artist) => Some(album_artist), 315 - None => Some(tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default()), 316 - }, 317 - "duration": duration, 318 - "trackNumber": tag.track(), 319 - "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 320 - true => Some(date), 321 - false => None, 322 - }), 323 - "year": tag.year(), 324 - "discNumber": tag.disk().map(|disc| match disc { 325 - 0 => Some(1), 326 - _ => Some(disc), 327 - }).unwrap_or(Some(1)), 328 - "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 329 - "albumArt": match albumart { 330 - Some(albumart) => Some(format!("https://cdn.rocksky.app/covers/{}", albumart)), 331 - None => None 332 - }, 333 - "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 334 - "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 335 - })) 336 - .send() 337 - .await?; 310 + .post(URL) 311 + .header("Authorization", format!("Bearer {}", access_token)) 312 + .json(&serde_json::json!({ 313 + "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 314 + "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 315 + "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 316 + "albumArtist": match tag.get_string(&lofty::tag::ItemKey::AlbumArtist) { 317 + Some(album_artist) => Some(album_artist), 318 + None => Some(tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default()), 319 + }, 320 + "duration": duration, 321 + "trackNumber": tag.track(), 322 + "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 323 + true => Some(date), 324 + false => None, 325 + }), 326 + "year": tag.year(), 327 + "discNumber": tag.disk().map(|disc| match disc { 328 + 0 => Some(1), 329 + _ => Some(disc), 330 + }).unwrap_or(Some(1)), 331 + "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 332 + "albumArt": match albumart { 333 + Some(albumart) => Some(format!("https://cdn.rocksky.app/covers/{}", albumart)), 334 + None => None 335 + }, 336 + "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 337 + "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 338 + })) 339 + .send() 340 + .await?; 338 341 println!("Track Saved: {} {}", title, response.status()); 339 342 tokio::time::sleep(std::time::Duration::from_secs(3)).await; 340 343 341 344 let track = get_track_by_hash(&pool, &hash).await?; 342 345 if let Some(track) = track { 343 346 let status = 344 - create_google_drive_path(&pool, &file, &track, &google_drive_id).await; 347 + create_google_drive_path(&pool, &file, &track, &google_drive_id, &file_id).await; 345 348 346 349 println!("status: {:?}", status); 350 + 351 + // TODO: publish file metadata to nats 347 352 348 353 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 349 354
+17
crates/googledrive/src/xata/google_drive_directory.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] 5 + pub struct GoogleDriveDirectory { 6 + pub xata_id: String, 7 + pub name: String, 8 + pub path: String, 9 + pub file_id: String, 10 + pub parent_id: Option<String>, 11 + pub google_drive_id: String, 12 + pub xata_version: i32, 13 + #[serde(with = "chrono::serde::ts_seconds")] 14 + pub xata_createdat: DateTime<Utc>, 15 + #[serde(with = "chrono::serde::ts_seconds")] 16 + pub xata_updatedat: DateTime<Utc>, 17 + }
+1
crates/googledrive/src/xata/google_drive_path.rs
··· 6 6 pub xata_id: String, 7 7 pub google_drive_id: String, 8 8 pub track_id: String, 9 + pub directory_id: Option<String>, 9 10 pub xata_version: i32, 10 11 #[serde(with = "chrono::serde::ts_seconds")] 11 12 pub xata_createdat: DateTime<Utc>,
+1
crates/googledrive/src/xata/mod.rs
··· 1 1 pub mod google_drive; 2 + pub mod google_drive_directory; 2 3 pub mod google_drive_path; 3 4 pub mod google_drive_token; 4 5 pub mod track;