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_yaml::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)]
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 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);
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        }
63    } else if !config.require_match && !anime_list.is_empty() {
64        // No match found, falling back to the latest updated one
65        let anime = &anime_list[0];
66
67        if (anime_episode != &anime.total_episodes || config.scrobble_last_ep)
68            && anime_episode > &anime.watched_episodes
69        {
70            mal_api::update_anime_details(config, &anime.id, &anime.total_episodes, anime_episode);
71        }
72    } else {
73        eprintln!("Currently watching anime list is empty");
74    }
75}
76
77fn check_filters(config: &Config, payload: &Json<Value>) -> bool {
78    // Check if the webhook was about scrobbling
79    if payload["event"].as_str().unwrap_or("") != "media.scrobble" {
80        return false;
81    }
82
83    // Check if user filters match
84    if config.plex_users.is_some() {
85        let user = payload["Account"]["title"]
86            .as_str()
87            .unwrap_or("")
88            .to_string();
89
90        if !config.plex_users.contains(user) {
91            return false;
92        }
93    }
94
95    // Check if library filters match
96    if config.plex_libraries.is_some() {
97        let library = payload["Metadata"]["librarySectionTitle"]
98            .as_str()
99            .unwrap_or("")
100            .to_string();
101
102        if !config.plex_libraries.contains(library) {
103            return false;
104        }
105    }
106    true
107}