diff --git a/Cargo.lock b/Cargo.lock index 4e41594..e77d4c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,9 +124,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.24" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ "shlex", ] @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "danmaku" -version = "1.7.22" +version = "1.7.30" dependencies = [ "anyhow", "bincode", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -319,15 +319,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -336,15 +336,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -353,21 +353,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -750,12 +750,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" @@ -831,12 +828,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - [[package]] name = "powerfmt" version = "0.2.0" @@ -1025,9 +1016,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", "rustls-pki-types", @@ -1070,9 +1061,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index a7be896..457da7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "danmaku" -version = "1.7.22" +version = "1.7.30" authors = ["rkscv", "kosette"] edition = "2021" rust-version = "1.81" diff --git a/src/dandanplay.rs b/src/dandanplay.rs index 2f49587..064a763 100644 --- a/src/dandanplay.rs +++ b/src/dandanplay.rs @@ -1,4 +1,5 @@ -use crate::utils::CLIENT; +use crate::emby::get_episode_num_emby; +use crate::utils::{Linkage, CLIENT}; use crate::{ emby::{get_episode_info, get_series_info, EpInfo}, mpv::osd_message, @@ -180,40 +181,72 @@ pub async fn get_danmaku(path: &str, filter: Arc) -> Result let mut episode_id = 0usize; if linkage.items.is_empty() { - let epid = get_episode_id_by_info(&ep_info, path).await; + let epid = get_episode_id_by_info(&ep_info, &mut linkage).await; match epid { Ok(p) => episode_id = p, Err(_) => { osd_message("trying matching with video hash"); episode_id = - get_episode_id_by_hash(&get_stream_hash(path).await?, &file_name) - .await? + match get_episode_id_by_hash(&get_stream_hash(path).await?, &file_name) + .await + { + Ok(id) => { + if get_episode_num_dan(id).await? + == get_episode_num_emby(&ep_info).await? + { + linkage.insert_seasons( + &ep_info.host, + &ep_info.item_info.se_id, + id / 10000, + ); + } + id + } + Err(e) => return Err(e), + } } } - linkage.insert(&ep_info.host, &ep_info.item_id, episode_id); + linkage.insert_items(&ep_info.host, &ep_info.item_info.item_id, episode_id); linkage.save_as_bincode().await?; - } - - let epid = linkage.get(&ep_info.host, &ep_info.item_id); - - if epid.is_none() { - let epid = get_episode_id_by_info(&ep_info, path).await; - - match epid { - Ok(p) => episode_id = p, - Err(_) => { - osd_message("trying matching with video hash"); - episode_id = - get_episode_id_by_hash(&get_stream_hash(path).await?, &file_name) - .await? + } else { + let epid = linkage.get_items(&ep_info.host, &ep_info.item_info.item_id); + + if epid.is_none() { + let epid = get_episode_id_by_info(&ep_info, &mut linkage).await; + + match epid { + Ok(p) => episode_id = p, + Err(_) => { + osd_message("trying matching with video hash"); + episode_id = match get_episode_id_by_hash( + &get_stream_hash(path).await?, + &file_name, + ) + .await + { + Ok(id) => { + if get_episode_num_dan(id).await? + == get_episode_num_emby(&ep_info).await? + { + linkage.insert_seasons( + &ep_info.host, + &ep_info.item_info.se_id, + id / 10000, + ); + } + id + } + Err(e) => return Err(e), + } + } } - } - linkage.insert(&ep_info.host, &ep_info.item_id, episode_id); - linkage.save_as_bincode().await?; - } else if let Some(id) = epid { - episode_id = id + linkage.insert_items(&ep_info.host, &ep_info.item_info.item_id, episode_id); + linkage.save_as_bincode().await?; + } else if let Some(id) = epid { + episode_id = id + } } if episode_id == 0usize { @@ -223,7 +256,6 @@ pub async fn get_danmaku(path: &str, filter: Arc) -> Result episode_id } else { osd_message("trying matching with video hash"); - get_episode_id_by_hash(&get_stream_hash(path).await?, &file_name).await? } }; @@ -319,19 +351,24 @@ async fn get_episode_id_by_hash(hash: &str, file_name: &str) -> Result { // total shit // shitshitshitshitshitshitshitshitshitshitshit // -async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result { +async fn get_episode_id_by_info(ep_info: &EpInfo, linkage: &mut Linkage) -> Result { use crate::utils::{get_dan_sum, get_em_sum, SearchRes}; use std::result::Result::Ok; use url::form_urlencoded; - let encoded_name: String = - form_urlencoded::byte_serialize(ep_info.get_series_name().as_bytes()).collect(); - let ep_type = &ep_info.r#type; - let ep_snum = ep_info.sn_index; - let ep_num = ep_info.ep_index; - let sid = &ep_info.ss_id; + let host = &ep_info.host; + let ep_snum = ep_info.item_info.sn_index; + let ep_num = ep_info.item_info.ep_index; + let seid = &ep_info.item_info.se_id; + + let anime_id = linkage.get_seasons(host, seid); + if let Some(id) = anime_id { + return Ok(format!("{}{:04}", id, ep_num).parse::()?); + } + let encoded_name: String = + form_urlencoded::byte_serialize(ep_info.get_series_name().as_bytes()).collect(); let url = format!( "https://api.dandanplay.net/api/v2/search/anime?keyword={}&type={}", encoded_name, ep_type @@ -360,7 +397,7 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result Result()?); }; - let ep_num_list = get_series_info(video_url, sid).await?; + let ep_num_list = get_series_info(ep_info).await?; if ep_num_list.is_empty() { error!("Ooops, series info fetching from Emby is empty"); @@ -410,6 +447,8 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result()?); }; @@ -418,6 +457,8 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result()?); } @@ -439,6 +480,8 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result()?); } @@ -451,6 +494,8 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result()?); } else { (ani_id, ep_id) = ( @@ -493,7 +538,7 @@ async fn get_episode_id_by_info(ep_info: &EpInfo, video_url: &str) -> Result Result Result Result Result()?) } + +#[derive(Debug, Deserialize)] +struct Bangumi { + bangumi: BEpisodes, +} + +#[derive(Debug, Deserialize)] +struct BEpisodes { + episodes: Vec, +} + +#[derive(Deserialize, Debug)] +struct BEpisode { + #[serde(rename = "episodeNumber")] + episode_number: String, +} + +pub async fn get_episode_num_dan(epid: usize) -> Result { + let anime_id = epid / 10000; + let bangumi_url = format!("https://api.dandanplay.net/api/v2/bangumi/{}", anime_id); + let res = CLIENT.get(bangumi_url).send().await?; + + if !res.status().is_success() { + error!( + "Failed to fetch seasons info from Emby server, Status: {:?}", + res.status() + ); + + return Err(anyhow!( + "fetch seasons info error, status: {}", + res.status() + )); + } + + let episodes = res.json::().await?; + let mut sum = 0; + + let _ = episodes.bangumi.episodes.iter().map(|ep| { + if ep.episode_number.parse::().is_ok() { + sum += 1; + } + }); + Ok(sum) +} diff --git a/src/emby.rs b/src/emby.rs index e713e13..5e94377 100644 --- a/src/emby.rs +++ b/src/emby.rs @@ -55,30 +55,47 @@ pub(crate) fn extract_params(video_url: &str) -> Result { } #[derive(Debug)] -pub(crate) struct EpInfo { - pub r#type: String, - pub host: String, +pub(crate) struct ItemInfo { pub name: String, - pub s_name: String, + pub ss_name: String, pub ep_index: u64, pub sn_index: i64, pub ss_id: String, + pub se_id: String, pub item_id: String, - pub status: bool, } -impl Default for EpInfo { +impl Default for ItemInfo { fn default() -> Self { Self { - r#type: "unknown".to_string(), name: "unknown".to_string(), - s_name: "unknown".to_string(), + ss_name: "unknown".to_string(), ep_index: 0, sn_index: -1, ss_id: "0".to_string(), + se_id: "0".to_string(), item_id: "0".to_string(), - status: false, + } + } +} + +#[derive(Debug)] +pub(crate) struct EpInfo { + pub r#type: String, + pub host: String, + pub api_key: String, + pub item_info: ItemInfo, + pub status: bool, +} + +impl Default for EpInfo { + fn default() -> Self { + Self { + r#type: "unknown".to_string(), host: "unknown".to_string(), + api_key: "unknown".to_string(), + item_info: ItemInfo::default(), + status: false, } } } @@ -86,8 +103,8 @@ impl Default for EpInfo { impl Display for EpInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = format!( - "[Type: {} Name: {} Series Name: {} Season Number: {} Episode Number: {} SeriesId: {} Status: {}]", - self.r#type, self.name,self.s_name, self.sn_index, self.ep_index, self.ss_id, self.status + "[Type: {} Name: {} Series Name: {} Season Number: {} Episode Number: {} SeriesId: {} SeasonId: {} Status: {}]", + self.r#type, self.item_info.name,self.item_info.ss_name, self.item_info.sn_index, self.item_info.ep_index, self.item_info.ss_id, self.item_info.se_id, self.status ); write!(f, "{}", str) @@ -97,17 +114,17 @@ impl Display for EpInfo { impl EpInfo { pub fn get_name(&self) -> String { if self.r#type == "tvseries" || self.r#type == "ova" { - format!("{} {}", self.s_name, self.name) + format!("{} {}", self.item_info.ss_name, self.item_info.name) } else { - self.name.to_string() + self.item_info.name.to_string() } } pub fn get_series_name(&self) -> String { if self.r#type == "tvseries" || self.r#type == "ova" { - self.s_name.to_string() + self.item_info.ss_name.to_string() } else { - self.name.to_string() + self.item_info.name.to_string() } } } @@ -125,13 +142,15 @@ struct EpDatum { #[serde(default, rename = "Name")] name: String, #[serde(default, rename = "SeriesName")] - s_name: String, + series_name: String, #[serde(default, rename = "ParentIndexNumber")] - s_index: i64, + season_index: i64, #[serde(default, rename = "IndexNumber")] - e_index: u64, + ep_index: u64, #[serde(default, rename = "SeriesId")] - s_id: String, + series_id: String, + #[serde(default, rename = "SeasonId")] + season_id: String, } impl Default for EpDatum { @@ -139,10 +158,11 @@ impl Default for EpDatum { Self { r#type: "unknown".to_string(), name: "unknown".to_string(), - s_name: "unknown".to_string(), - s_index: -1, - e_index: 0, - s_id: "0".to_string(), + series_name: "unknown".to_string(), + season_index: -1, + ep_index: 0, + series_id: "0".to_string(), + season_id: "0".to_string(), } } } @@ -163,7 +183,7 @@ pub(crate) async fn get_episode_info(video_url: &str) -> Result { let response = CLIENT .get(url) - .header("X-Emby-Token", api_key) + .header("X-Emby-Token", &api_key) .send() .await?; @@ -185,39 +205,51 @@ pub(crate) async fn get_episode_info(video_url: &str) -> Result { .context("can not parse episode info")?; if epdata.items[0].r#type == "Episode" { - if epdata.items[0].s_index == 0 { + if epdata.items[0].season_index == 0 { Ok(EpInfo { r#type: "ova".to_string(), - name: epdata.items[0].name.clone(), - s_name: epdata.items[0].s_name.clone(), - sn_index: epdata.items[0].s_index, - ep_index: epdata.items[0].e_index, - ss_id: epdata.items[0].s_id.clone(), - status: true, host, - item_id, + api_key, + item_info: ItemInfo { + name: epdata.items[0].name.clone(), + ss_name: epdata.items[0].series_name.clone(), + sn_index: epdata.items[0].season_index, + ep_index: epdata.items[0].ep_index, + ss_id: epdata.items[0].series_id.clone(), + se_id: epdata.items[0].season_id.clone(), + item_id, + }, + status: true, }) } else { Ok(EpInfo { r#type: "tvseries".to_string(), - name: epdata.items[0].name.clone(), - s_name: epdata.items[0].s_name.clone(), - sn_index: epdata.items[0].s_index, - ep_index: epdata.items[0].e_index, - ss_id: epdata.items[0].s_id.clone(), - status: true, host, - item_id, + api_key, + item_info: ItemInfo { + name: epdata.items[0].name.clone(), + ss_name: epdata.items[0].series_name.clone(), + sn_index: epdata.items[0].season_index, + ep_index: epdata.items[0].ep_index, + ss_id: epdata.items[0].series_id.clone(), + se_id: epdata.items[0].season_id.clone(), + item_id, + }, + status: true, }) } } else if epdata.items[0].r#type == "Movie" { Ok(EpInfo { r#type: "movie".to_string(), - name: epdata.items[0].name.clone(), - status: true, host, - item_id, - ..Default::default() + api_key, + item_info: ItemInfo { + name: epdata.items[0].name.clone(), + item_id, + ..Default::default() + }, + + status: true, }) } else { Ok(EpInfo::default()) @@ -254,10 +286,12 @@ struct Episode { /// a list containing number of episodes and season number of every season except S0 /// -pub(crate) async fn get_series_info(video_url: &str, series_id: &str) -> Result> { +pub(crate) async fn get_series_info(ep_info: &EpInfo) -> Result> { use std::result::Result::Ok; - let P3 { host, api_key, .. } = extract_params(video_url).context("not emby url")?; + let host = ep_info.host.clone(); + let api_key = ep_info.api_key.clone(); + let series_id = ep_info.item_info.ss_id.clone(); let seasons_url = format!("{}/emby/Shows/{}/Seasons?reqformat=json", host, series_id); @@ -333,3 +367,48 @@ pub(crate) async fn get_series_info(video_url: &str, series_id: &str) -> Result< Ok(episodes_list) } + +pub(crate) async fn get_episode_num_emby(ep_info: &EpInfo) -> Result { + let series_id = ep_info.item_info.ss_id.clone(); + let season_id = ep_info.item_info.se_id.clone(); + let host = ep_info.host.clone(); + let api_key = ep_info.api_key.clone(); + + let url = format!( + "{}/emby/Shows/{}/Episodes?SeasonId={}&reqformat=json", + host, series_id, season_id + ); + + let res = CLIENT + .get(url) + .header("X-Emby-Token", &api_key) + .send() + .await?; + + if !res.status().is_success() { + error!( + "Failed to fetch seasons info from Emby server, Status: {:?}", + res.status() + ); + + return Err(anyhow!( + "fetch seasons info error, status: {}", + res.status() + )); + } + + let episodes = res + .json::() + .await + .context("can not parse episodes info")?; + + let mut sum = 0; + for ep in episodes.items { + // shit + if ep.season_num != 0 && ep.ep_num > sum { + sum += 1; + } + } + + Ok(sum) +} diff --git a/src/lib.rs b/src/lib.rs index d84bc4b..40adcc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,7 @@ async fn main() -> c_int { .1; // Initialize tracing subscriber - if ["true", "on", "enable"].contains(&options.log) { + if ["true", "on", "enable"].contains(&options.log.to_ascii_lowercase().as_str()) { let log_dir = expand_path("~~/files").expect("can not expand log_dir"); if !std::path::Path::new(&log_dir).exists() { diff --git a/src/utils.rs b/src/utils.rs index 8717de7..3e6b36e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,7 +4,12 @@ use hex::encode; use md5::{Digest, Md5}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::LazyLock}; +use std::{ + borrow::Borrow, + collections::{HashMap, VecDeque}, + hash::Hash, + sync::LazyLock, +}; use tracing::{error, info}; pub(crate) static CLIENT: LazyLock = LazyLock::new(build); @@ -172,7 +177,8 @@ pub struct TimesId { #[derive(Serialize, Deserialize, Debug)] pub struct Linkage { - pub items: HashMap>, + pub items: HashMap>, + pub seasons: HashMap>, } impl Default for Linkage { @@ -185,10 +191,11 @@ impl Linkage { pub fn new() -> Self { Linkage { items: HashMap::new(), + seasons: HashMap::new(), } } - pub fn insert(&mut self, host_key: &str, item_id: &str, epid: usize) { + pub fn insert_items(&mut self, host_key: &str, item_id: &str, epid: usize) { let timestamped_value = TimesId { epid, last_updated: SystemTime::now(), @@ -199,14 +206,25 @@ impl Linkage { .insert(item_id.to_string(), timestamped_value); } - pub fn get(&self, host_key: &str, item_id: &str) -> Option { + pub fn get_items(&self, host_key: &str, item_id: &str) -> Option { self.items.get(host_key)?.get(item_id).map(|tv| tv.epid) } + pub fn insert_seasons(&mut self, host_key: &str, season_id: &str, anime_id: usize) { + self.seasons + .entry(host_key.to_string()) + .or_default() + .insert(season_id.to_string(), anime_id); + } + + pub fn get_seasons(&self, host_key: &str, season_id: &str) -> Option { + self.seasons.get(host_key)?.get(season_id).copied() + } + pub fn clean_expired_entries(&mut self, expiration_duration: Duration) { let now = SystemTime::now(); self.items.retain(|_, inner_map| { - inner_map.retain(|_, timestamped_value| { + inner_map.map.retain(|_, timestamped_value| { now.duration_since(timestamped_value.last_updated) .map(|age| age < expiration_duration) .unwrap_or(true) @@ -221,9 +239,7 @@ impl Linkage { use tokio::io::AsyncWriteExt; let encoded: Vec = bincode::serialize(self)?; - let path_str = expand_path("~~/files/danmaku/database")?; - let path = Path::new(&path_str); if !path.parent().expect("no parent dir").exists() { @@ -260,3 +276,61 @@ impl Linkage { Ok(linkage) } } + +#[derive(Debug, Deserialize, Serialize)] +pub struct LimitedHashMap +where + K: Clone + std::hash::Hash + Eq, +{ + map: HashMap, + keys: VecDeque, + capacity: usize, +} + +impl Default for LimitedHashMap { + fn default() -> Self { + Self::new(30) + } +} + +impl LimitedHashMap { + fn new(capacity: usize) -> Self { + LimitedHashMap { + map: HashMap::new(), + keys: VecDeque::new(), + capacity, + } + } + + fn insert(&mut self, key: K, value: V) { + if self.map.contains_key(&key) { + self.map.insert(key.clone(), value); + } else { + if self.keys.len() == self.capacity { + if let Some(oldest_key) = self.keys.pop_front() { + self.map.remove(&oldest_key); + } + } + + self.keys.push_back(key.clone()); + self.map.insert(key, value); + } + } + + fn get(&self, key: &Q) -> Option<&V> + where + Q: ?Sized, + K: Borrow, + Q: Hash + Eq, + { + self.map.get(key) + } + + fn _len(&self) -> usize { + self.map.len() + } + + fn is_empty(&self) -> bool { + self.map.is_empty() + } +}