plex_mal_scrobbler/
lib.rs

1use lazy_static::lazy_static;
2use rocket::{form::validate::Contains, serde::json::Json};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::{fs::File, sync::Mutex};
6
7pub mod mal_api;
8
9lazy_static! {
10    pub static ref CONFIG_PATH: String = {
11        match home::home_dir() {
12            Some(path) => format!("{}/.config/plex-mal-scrobbler/config.yml", path.display()),
13            None => "/etc/plex-mal-scrobbler/config.yml".to_string(),
14        }
15    };
16    pub static ref CONFIG: Mutex<Config> = {
17        serde_yml::from_reader(
18            File::open(CONFIG_PATH.as_str())
19                .unwrap_or_else(|_| panic!("Couldn't find {}", CONFIG_PATH.as_str())),
20        )
21        .expect("Failed to parse the config.yml")
22    };
23}
24
25#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
26pub struct Config {
27    pub mal_client_id: String,
28    pub mal_access_token: Option<String>,
29    pub mal_refresh_token: Option<String>,
30    pub mal_token_expires_in: Option<i64>,
31    pub plex_users: Option<Vec<String>>,
32    pub plex_libraries: Option<Vec<String>>,
33    pub scrobble_last_ep: bool,
34    pub require_match: bool,
35    pub test: bool,
36    pub port: Option<u64>,
37}
38
39pub async fn scrobble(config: &Config, payload: &Json<Value>) {
40    if !check_filters(config, payload) {
41        return;
42    }
43
44    // Get anime title
45    let anime_title = &payload["Metadata"]["grandparentTitle"]
46        .as_str()
47        .unwrap()
48        .to_string()
49        .to_lowercase();
50    // Get current anime episode number
51    let anime_episode = &payload["Metadata"]["index"].as_u64().unwrap();
52
53    // Get user currently watching anime list
54    let anime_list = mal_api::get_user_anime_list(config).await;
55
56    if let Some(anime) = anime_list.iter().find(|a| a.titles.contains(anime_title)) {
57        // Found exact match from the synonyms
58        if (anime_episode != &anime.total_episodes || config.scrobble_last_ep)
59            && anime_episode > &anime.watched_episodes
60        {
61            mal_api::update_anime_details(config, &anime.id, &anime.total_episodes, anime_episode)
62                .await;
63        }
64    } else if !config.require_match && !anime_list.is_empty() {
65        // No match found, falling back to the latest updated one
66        let anime = &anime_list[0];
67
68        if (anime_episode != &anime.total_episodes || config.scrobble_last_ep)
69            && anime_episode > &anime.watched_episodes
70        {
71            mal_api::update_anime_details(config, &anime.id, &anime.total_episodes, anime_episode)
72                .await;
73        }
74    } else {
75        eprintln!("Currently watching anime list is empty");
76    }
77}
78
79fn check_filters(config: &Config, payload: &Json<Value>) -> bool {
80    // Check if the webhook was about scrobbling
81    if payload["event"].as_str().unwrap_or("") != "media.scrobble" {
82        return false;
83    }
84
85    // Check if user filters match
86    if config.plex_users.is_some() {
87        let user = payload["Account"]["title"]
88            .as_str()
89            .unwrap_or("")
90            .to_string();
91
92        if !config.plex_users.contains(user) {
93            return false;
94        }
95    }
96
97    // Check if library filters match
98    if config.plex_libraries.is_some() {
99        let library = payload["Metadata"]["librarySectionTitle"]
100            .as_str()
101            .unwrap_or("")
102            .to_string();
103
104        if !config.plex_libraries.contains(library) {
105            return false;
106        }
107    }
108    true
109}