dango-music-player/src/music_storage/db_reader/itunes/reader.rs
2024-02-16 22:24:02 -05:00

348 lines
No EOL
12 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use file_format::FileFormat;
use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt};
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use uuid::Uuid;
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use std::time::Duration as StdDur;
use std::vec::Vec;
use chrono::prelude::*;
use crate::config::config::{Config, ConfigLibrary};
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
use crate::music_storage::library::{AlbumArt, MusicLibrary, Service, Song, Tag, URI, BannedType};
use crate::music_storage::utils;
use urlencoding::decode;
#[derive(Debug, Default, Clone)]
pub struct ITunesLibrary {
tracks: Vec<ITunesSong>,
}
impl ITunesLibrary {
fn new() -> Self {
Default::default()
}
pub fn tracks(self) -> Vec<ITunesSong> {
self.tracks
}
}
impl ExternalLibrary for ITunesLibrary {
fn from_file(file: &Path) -> Self {
let mut reader = Reader::from_file(file).unwrap();
reader.trim_text(true);
//count every event, for fun ig?
let mut count = 0;
//count for skipping useless beginning key
let mut count2 = 0;
//number of grabbed songs
let mut count3 = 0;
//number of IDs skipped
let mut count4 = 0;
let mut buf = Vec::new();
let mut skip = false;
let mut converted_songs: Vec<ITunesSong> = Vec::new();
let mut song_tags: HashMap<String, String> = HashMap::new();
let mut key: String = String::new();
let mut tagvalue: String = String::new();
let mut key_selected = false;
use std::time::Instant;
let now = Instant::now();
loop {
//push tag to song_tags map
if !key.is_empty() && !tagvalue.is_empty() {
song_tags.insert(key.clone(), tagvalue.clone());
key.clear();
tagvalue.clear();
key_selected = false;
//end the song to start a new one, and turn turn current song map into iTunesSong
if song_tags.contains_key(&"Location".to_string()) {
count3 += 1;
//check for skipped IDs
if &count3.to_string()
!= song_tags.get_key_value(&"Track ID".to_string()).unwrap().1
{
count3 += 1;
count4 += 1;
}
converted_songs.push(ITunesSong::from_hashmap(&mut song_tags).unwrap());
song_tags.clear();
skip = true;
}
}
match reader.read_event_into(&mut buf) {
Ok(Event::Start(_)) => {
count += 1;
count2 += 1;
}
Ok(Event::Text(e)) => {
if count < 17 && count != 10 {
continue;
} else if skip {
skip = false;
continue;
}
let text = e.unescape().unwrap().to_string();
if text == count2.to_string() && !key_selected {
continue;
}
//Add the key/value depenidng on if the key is selected or not ⛩sorry buzz
match key_selected {
true => tagvalue.push_str(&text),
false => {
key.push_str(&text);
if !key.is_empty() {
key_selected = true
} else {
panic!("Key not selected?!")
}
}
}
}
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
Ok(Event::Eof) => break,
_ => (),
}
buf.clear();
}
let elasped = now.elapsed();
println!("\n\niTunesReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}", count3, elasped.as_secs(), count4);
let mut lib = ITunesLibrary::new();
lib.tracks.append(converted_songs.as_mut());
lib
}
fn to_songs(&self) -> Vec<crate::music_storage::library::Song> {
let mut count = 0;
let mut bun: Vec<Song> = Vec::new();
for track in &self.tracks {
//grab "other" tags
let mut tags_: BTreeMap<Tag, String> = BTreeMap::new();
for (key, val) in &track.tags {
tags_.insert(to_tag(key.clone()), val.clone());
}
//make the path readable
let loc_ = if track.location.contains("file://localhost/") {
decode(track.location.strip_prefix("file://localhost/").unwrap())
.unwrap()
.into_owned()
} else {
decode(track.location.as_str()).unwrap().into_owned()
};
let loc = loc_.as_str();
if File::open(loc).is_err() && !loc.contains("http") {
count += 1;
dbg!(loc);
continue;
}
let location: URI = if track.location.contains("file://localhost/") {
URI::Local(PathBuf::from(
decode(track.location.strip_prefix("file://localhost/").unwrap())
.unwrap()
.into_owned()
.as_str(),
))
} else {
URI::Remote(Service::None, decode(&track.location).unwrap().into_owned())
};
let dur = match get_duration(Path::new(&loc)) {
Ok(e) => e,
Err(e) => {
dbg!(e);
StdDur::from_secs(0)
}
};
let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs());
let ny: Song = Song {
location,
uuid: Uuid::new_v4(),
plays: track.plays,
skips: 0,
favorited: track.favorited,
// banned: if track.banned {
// Some(BannedType::All)
// }else {
// None
// },
rating: track.rating,
format: match FileFormat::from_file(PathBuf::from(&loc)) {
Ok(e) => Some(e),
Err(_) => None,
},
duration: dur,
play_time: play_time_,
last_played: track.last_played,
date_added: track.date_added,
date_modified: track.date_modified,
album_art: match get_art(Path::new(&loc)) {
Ok(e) => e,
Err(_) => Vec::new(),
},
tags: tags_,
};
// dbg!(&ny.tags);
bun.push(ny);
}
println!("skipped: {}", count);
bun
}
}
fn to_tag(string: String) -> Tag {
match string.to_lowercase().as_str() {
"name" => Tag::Title,
"album" => Tag::Album,
"artist" => Tag::Artist,
"album artist" => Tag::AlbumArtist,
"genre" => Tag::Genre,
"comment" => Tag::Comment,
"track number" => Tag::Track,
"disc number" => Tag::Disk,
_ => Tag::Key(string),
}
}
fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> {
let dur = match Probe::open(file)?.read() {
Ok(tagged_file) => tagged_file.properties().duration(),
Err(_) => StdDur::from_secs(0),
};
Ok(dur)
}
fn get_art(file: &Path) -> Result<Vec<AlbumArt>, LoftyError> {
let mut album_art: Vec<AlbumArt> = Vec::new();
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
let tagged_file: lofty::TaggedFile;
let tag = match Probe::open(file)?.options(normal_options).read() {
Ok(e) => {
tagged_file = e;
match tagged_file.primary_tag() {
Some(primary_tag) => primary_tag,
None => match tagged_file.first_tag() {
Some(first_tag) => first_tag,
None => blank_tag,
},
}
}
Err(_) => blank_tag,
};
let mut img = match utils::find_images(file) {
Ok(e) => e,
Err(_) => Vec::new(),
};
if !img.is_empty() {
album_art.append(img.as_mut());
}
for (i, _art) in tag.pictures().iter().enumerate() {
let new_art = AlbumArt::Embedded(i);
album_art.push(new_art)
}
Ok(album_art)
}
#[derive(Debug, Clone, Default)]
pub struct ITunesSong {
pub id: i32,
pub plays: i32,
pub favorited: bool,
pub banned: bool,
pub rating: Option<u8>,
pub format: Option<String>,
pub song_type: Option<String>,
pub last_played: Option<DateTime<Utc>>,
pub date_added: Option<DateTime<Utc>>,
pub date_modified: Option<DateTime<Utc>>,
pub tags: BTreeMap<String, String>,
pub location: String,
}
impl ITunesSong {
pub fn new() -> ITunesSong {
Default::default()
}
fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, LoftyError> {
let mut song = ITunesSong::new();
//get the path with the first bit chopped off
let path_: String = map.get_key_value("Location").unwrap().1.clone();
let track_type: String = map.get_key_value("Track Type").unwrap().1.clone();
let path: String = match track_type.as_str() {
"File" => {
if path_.contains("file://localhost/") {
path_.strip_prefix("file://localhost/").unwrap();
}
path_
}
"URL" => path_,
_ => path_,
};
for (key, value) in map {
match key.as_str() {
"Track ID" => song.id = value.parse().unwrap(),
"Location" => song.location = path.to_string(),
"Play Count" => song.plays = value.parse().unwrap(),
"Love" => {
//check if the track is (L)Loved or (B)Banned
match value.as_str() {
"L" => song.favorited = true,
"B" => song.banned = false,
_ => continue,
}
}
"Rating" => song.rating = Some(value.parse().unwrap()),
"Kind" => song.format = Some(value.to_string()),
"Play Date UTC" => {
song.last_played = Some(DateTime::<Utc>::from_str(value).unwrap())
}
"Date Added" => song.date_added = Some(DateTime::<Utc>::from_str(value).unwrap()),
"Date Modified" => {
song.date_modified = Some(DateTime::<Utc>::from_str(value).unwrap())
}
"Track Type" => song.song_type = Some(value.to_string()),
_ => {
song.tags.insert(key.to_string(), value.to_string());
}
}
}
// println!("{:.2?}", song);
Ok(song)
}
}
#[test]
fn itunes_lib_test() {
let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None);
config.libraries.libraries.push(config_lib.clone());
let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs();
let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap();
songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap());
config.write_file().unwrap();
library.save(config).unwrap();
}