├── .gitignore ├── setup.sh ├── assets ├── samples │ └── samka_sample.mp3 └── resources │ └── kitay brusnika - odinokaya samka.mp3 ├── Cargo.toml ├── src ├── db │ ├── mod.rs │ ├── initdb.sql │ └── pg.rs ├── lib.rs └── fingerprint.rs ├── LICENSE ├── README.MD └── examples └── ex.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/db 4 | createdb khalzam 5 | psql -f initdb.sql khalzam 6 | -------------------------------------------------------------------------------- /assets/samples/samka_sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisasexypantera94/khalzam-rs/HEAD/assets/samples/samka_sample.mp3 -------------------------------------------------------------------------------- /assets/resources/kitay brusnika - odinokaya samka.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisasexypantera94/khalzam-rs/HEAD/assets/resources/kitay brusnika - odinokaya samka.mp3 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "khalzam" 3 | version = "0.3.9" 4 | authors = ["kisasexypantera94 "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Simple audio recognition library, port of khalzam-go" 8 | repository = "https://github.com/kisasexypantera94/khalzam-rs" 9 | readme = "README.md" 10 | exclude = ["assets/**"] 11 | categories = ["multimedia", "multimedia::audio"] 12 | keywords = ["audio", "recognition", "shazam"] 13 | 14 | [dependencies] 15 | postgres = "0.16.0-rc.1" 16 | r2d2 = "0.8.5" 17 | r2d2_postgres = "0.15.0-rc.1" 18 | minimp3 = "0.3.2" 19 | rustfft = "3.0.0" 20 | 21 | [dev-dependencies] 22 | rayon = "1.1.0" -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | //! `db` module takes care of interaction with repository/database. 2 | pub mod pg; 3 | 4 | use std::error::Error; 5 | 6 | /// `Repository` is an abstraction of database containing fingerprints. 7 | pub trait Repository { 8 | /// Map hashes from hash_array to song. 9 | fn index(&self, song: &str, hash_array: &[usize]) -> Result<(), Box>; 10 | /// Find the most similar song by hashes. 11 | fn find(&self, hash_array: &[usize]) -> Result, Box>; // It may be better to replace String result with generic. 12 | /// Delete song from database. 13 | fn delete(&self, song: &str) -> Result>; 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kisasexypantera94 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Khalzam 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/cacf5d6c8e6743fab59209e24f58ca4f)](https://app.codacy.com/app/kisasexypantera94/khalzam-rs?utm_source=github.com&utm_medium=referral&utm_content=kisasexypantera94/khalzam-rs&utm_campaign=Badge_Grade_Dashboard) 3 | [![Latest Version](https://img.shields.io/crates/v/khalzam.svg)](https://crates.io/crates/khalzam) 4 | [![Latest Version](https://docs.rs/khalzam/badge.svg)](https://docs.rs/khalzam) 5 | ## About 6 | `khalzam` is an audio recognition library that makes it easy to index and recognize audio files. 7 | It focuses on speed, efficiency and simplicity. 8 | Its algrorithm is based on [this article](https://royvanrijn.com/blog/2010/06/creating-shazam-in-java/). 9 | 10 | ## TODO 11 | * Rethink the way hashes are stored – use inverted index 12 | * Try to solve less specific problems: 13 | * query by humming 14 | * animal sounds recognition 15 | 16 | ## Setup 17 | You need to create and initialize database: 18 | ```zsh 19 | $ sh ./setup.sh 20 | ``` 21 | 22 | ## Usage 23 | You can use the library either 24 | through the [API](https://github.com/kisasexypantera94/khalzam-rs/tree/master/examples) 25 | or using [khalzam-cli](https://github.com/kisasexypantera94/khalzam-cli) 26 | -------------------------------------------------------------------------------- /examples/ex.rs: -------------------------------------------------------------------------------- 1 | use khalzam::db::pg::PostgresRepo; 2 | use khalzam::MusicLibrary; 3 | 4 | use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; 5 | 6 | use std::env; 7 | use std::fs; 8 | use std::io::Write; 9 | 10 | fn main() { 11 | let user = env::var("USER").unwrap(); 12 | let config = format!("host=localhost dbname=khalzam user={}", user); 13 | let pgrepo = PostgresRepo::open(&config).unwrap(); 14 | let m_lib = MusicLibrary::new(pgrepo); 15 | 16 | let resources = fs::read_dir("assets/resources").unwrap(); 17 | let paths: Vec<_> = resources.collect(); 18 | paths.par_iter().for_each(|path| { 19 | if let Ok(path) = path { 20 | let name = String::from(path.path().file_name().unwrap().to_str().unwrap()); 21 | let path = String::from(path.path().to_str().unwrap()); 22 | let stdout = std::io::stdout(); 23 | match m_lib.add(&path) { 24 | Ok(()) => writeln!(&mut stdout.lock(), "Added {}", name), 25 | Err(e) => writeln!(&mut stdout.lock(), "Can't add {}: {}", name, e), 26 | } 27 | .unwrap(); 28 | } 29 | }); 30 | 31 | let samples = fs::read_dir("assets/samples").unwrap(); 32 | for path in samples { 33 | if let Ok(path) = path { 34 | let name = String::from(path.path().file_name().unwrap().to_str().unwrap()); 35 | let path = String::from(path.path().to_str().unwrap()); 36 | println!("Recognizing `{}` ...", name); 37 | println!("Best match: {}", m_lib.recognize(&path).unwrap()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `khalzam` is an audio recognition library 2 | //! that makes it easy to index and recognize audio files. 3 | //! It focuses on speed, efficiency and simplicity. 4 | //! 5 | //! Its algrorithm is based on [this article]. 6 | //! 7 | //! [this article]: https://royvanrijn.com/blog/2010/06/creating-shazam-in-java/ 8 | pub mod db; 9 | mod fingerprint; 10 | 11 | use db::Repository; 12 | use fingerprint::FingerprintHandle; 13 | 14 | use std::error::Error; 15 | use std::path::Path; 16 | 17 | /// `MusicLibrary` is the central structure of the algorithm. 18 | /// It is the link for fingerprinting and repository interaction. 19 | pub struct MusicLibrary 20 | where 21 | T: Repository, 22 | { 23 | repo: T, 24 | fp_handle: FingerprintHandle, 25 | } 26 | 27 | impl MusicLibrary 28 | where 29 | T: Repository, 30 | { 31 | /// Create a new instance of `MusicLibrary` 32 | pub fn new(repo: T) -> MusicLibrary { 33 | MusicLibrary { 34 | repo, 35 | fp_handle: FingerprintHandle::new(), 36 | } 37 | } 38 | 39 | /// Add song. 40 | pub fn add(&self, filename: &str) -> Result<(), Box> { 41 | check_extension(filename)?; 42 | 43 | let song = get_songname(filename)?; 44 | let hash_array = self.fp_handle.calc_fingerprint(filename)?; 45 | self.repo.index(&song, &hash_array) 46 | } 47 | 48 | /// Recognize song. It returns the songname of the closest match in repository. 49 | pub fn recognize(&self, filename: &str) -> Result> { 50 | check_extension(filename)?; 51 | 52 | let hash_array = self.fp_handle.calc_fingerprint(filename)?; 53 | match self.repo.find(&hash_array)? { 54 | Some(res) => Ok(res), 55 | None => Ok("No matchings".to_string()), 56 | } 57 | } 58 | 59 | /// Delete song. 60 | pub fn delete(&self, songname: &str) -> Result> { 61 | match self.repo.delete(songname)? { 62 | x if x > 0 => Ok("Successfully deleted".to_string()), 63 | _ => Ok("Song not found".to_string()), 64 | } 65 | } 66 | } 67 | 68 | /// Сheck whether it is possible to process a file. 69 | fn check_extension(filename: &str) -> Result<(), Box> { 70 | let path = Path::new(filename); 71 | let ext = match path.extension() { 72 | Some(e_osstr) => match e_osstr.to_str() { 73 | Some(e) => e, 74 | None => return Err(Box::from("Invalid extension")), 75 | }, 76 | None => return Err(Box::from("Invalid extension")), 77 | }; 78 | if ext != "mp3" { 79 | return Err(Box::from("Invalid extension")); 80 | } 81 | 82 | Ok(()) 83 | } 84 | 85 | /// Get file basename without extension 86 | fn get_songname(filename: &str) -> Result> { 87 | match Path::new(filename).file_stem() { 88 | Some(stem) => match stem.to_str() { 89 | Some(stem_str) => Ok(stem_str.to_string()), 90 | None => Err(Box::from(format!("can't convert {:?} to str", stem))), 91 | }, 92 | None => Err(Box::from("filename is empty")), 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_check_extension() { 102 | assert!(check_extension("good.mp3").is_ok()); 103 | assert!(check_extension("bad.pdf").is_err()); 104 | } 105 | 106 | #[test] 107 | fn test_get_songname() { 108 | assert_eq!(get_songname("some_name.mp3").unwrap(), "some_name"); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/db/initdb.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 11.4 6 | -- Dumped by pg_dump version 11.4 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | SET default_tablespace = ''; 20 | 21 | SET default_with_oids = false; 22 | 23 | -- 24 | -- Name: hashes; Type: TABLE; Schema: public; Owner: - 25 | -- 26 | 27 | CREATE TABLE public.hashes ( 28 | hid integer NOT NULL, 29 | hash bigint, 30 | "time" integer, 31 | sid integer 32 | ); 33 | 34 | 35 | -- 36 | -- Name: hashes_hid_seq; Type: SEQUENCE; Schema: public; Owner: - 37 | -- 38 | 39 | CREATE SEQUENCE public.hashes_hid_seq 40 | AS integer 41 | START WITH 1 42 | INCREMENT BY 1 43 | NO MINVALUE 44 | NO MAXVALUE 45 | CACHE 1; 46 | 47 | 48 | -- 49 | -- Name: hashes_hid_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 50 | -- 51 | 52 | ALTER SEQUENCE public.hashes_hid_seq OWNED BY public.hashes.hid; 53 | 54 | 55 | -- 56 | -- Name: songs; Type: TABLE; Schema: public; Owner: - 57 | -- 58 | 59 | CREATE TABLE public.songs ( 60 | sid integer NOT NULL, 61 | song text 62 | ); 63 | 64 | 65 | -- 66 | -- Name: songs_sid_seq; Type: SEQUENCE; Schema: public; Owner: - 67 | -- 68 | 69 | CREATE SEQUENCE public.songs_sid_seq 70 | AS integer 71 | START WITH 1 72 | INCREMENT BY 1 73 | NO MINVALUE 74 | NO MAXVALUE 75 | CACHE 1; 76 | 77 | 78 | -- 79 | -- Name: songs_sid_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 80 | -- 81 | 82 | ALTER SEQUENCE public.songs_sid_seq OWNED BY public.songs.sid; 83 | 84 | 85 | -- 86 | -- Name: hashes hid; Type: DEFAULT; Schema: public; Owner: - 87 | -- 88 | 89 | ALTER TABLE ONLY public.hashes ALTER COLUMN hid SET DEFAULT nextval('public.hashes_hid_seq'::regclass); 90 | 91 | 92 | -- 93 | -- Name: songs sid; Type: DEFAULT; Schema: public; Owner: - 94 | -- 95 | 96 | ALTER TABLE ONLY public.songs ALTER COLUMN sid SET DEFAULT nextval('public.songs_sid_seq'::regclass); 97 | 98 | 99 | -- 100 | -- Name: hashes hashes_pkey; Type: CONSTRAINT; Schema: public; Owner: - 101 | -- 102 | 103 | ALTER TABLE ONLY public.hashes 104 | ADD CONSTRAINT hashes_pkey PRIMARY KEY (hid); 105 | 106 | 107 | -- 108 | -- Name: songs songs_pkey; Type: CONSTRAINT; Schema: public; Owner: - 109 | -- 110 | 111 | ALTER TABLE ONLY public.songs 112 | ADD CONSTRAINT songs_pkey PRIMARY KEY (sid); 113 | 114 | 115 | -- 116 | -- Name: songs songs_song_key; Type: CONSTRAINT; Schema: public; Owner: - 117 | -- 118 | 119 | ALTER TABLE ONLY public.songs 120 | ADD CONSTRAINT songs_song_key UNIQUE (song); 121 | 122 | 123 | -- 124 | -- Name: hashes_hash_time_sid_idx; Type: INDEX; Schema: public; Owner: - 125 | -- 126 | 127 | CREATE UNIQUE INDEX hashes_hash_time_sid_idx ON public.hashes USING btree (hash, "time", sid); 128 | 129 | 130 | -- 131 | -- Name: hashes_time_sid_idx; Type: INDEX; Schema: public; Owner: - 132 | -- 133 | 134 | CREATE UNIQUE INDEX hashes_time_sid_idx ON public.hashes USING btree ("time", sid); 135 | 136 | 137 | -- 138 | -- Name: hashes sid; Type: FK CONSTRAINT; Schema: public; Owner: - 139 | -- 140 | 141 | ALTER TABLE ONLY public.hashes 142 | ADD CONSTRAINT sid FOREIGN KEY (sid) REFERENCES public.songs(sid) ON UPDATE CASCADE ON DELETE CASCADE; 143 | 144 | 145 | -- 146 | -- PostgreSQL database dump complete 147 | -- 148 | 149 | -------------------------------------------------------------------------------- /src/fingerprint.rs: -------------------------------------------------------------------------------- 1 | //! `fingerprint` module takes care of audio decoding and creating acoustic fingerprints. 2 | use minimp3::{Decoder, Frame}; 3 | use rustfft::algorithm::Radix4; 4 | use rustfft::num_complex::Complex; 5 | use rustfft::num_traits::Zero; 6 | use rustfft::FFT; 7 | 8 | use std::error::Error; 9 | use std::fs::File; 10 | 11 | const FFT_WINDOW_SIZE: usize = 4096; 12 | const FREQ_BINS: &[usize] = &[40, 80, 120, 180, 300]; 13 | const FREQ_BIN_FIRST: usize = 40; 14 | const FREQ_BIN_LAST: usize = 300; 15 | const FUZZ_FACTOR: usize = 2; 16 | 17 | /// Helper struct for calculating acoustic fingerprint 18 | pub struct FingerprintHandle { 19 | /// FFT algorithm 20 | fft: Radix4, 21 | } 22 | 23 | impl FingerprintHandle { 24 | pub fn new() -> FingerprintHandle { 25 | FingerprintHandle { 26 | fft: Radix4::new(FFT_WINDOW_SIZE, false), 27 | } 28 | } 29 | 30 | pub fn calc_fingerprint(&self, filename: &str) -> Result, Box> { 31 | let pcm_f32 = decode_mp3(filename)?; 32 | 33 | let hash_array = pcm_f32 34 | .chunks_exact(FFT_WINDOW_SIZE) 35 | .map(|chunk| { 36 | let mut input: Vec> = chunk.iter().map(Complex::from).collect(); 37 | let mut output: Vec> = vec![Complex::zero(); FFT_WINDOW_SIZE]; 38 | self.fft.process(&mut input, &mut output); 39 | 40 | get_key_points(&output) 41 | }) 42 | .collect(); 43 | 44 | Ok(hash_array) 45 | } 46 | } 47 | 48 | /// Mp3 decoding function. 49 | /// 50 | /// Decoding is done using `minimp3.` 51 | /// Samples are read frame by frame and pushed to the vector. 52 | /// Conversion to mono is done by simply taking the mean of left and right channels. 53 | fn decode_mp3(filename: &str) -> Result, Box> { 54 | let mut decoder = Decoder::new(File::open(filename)?); 55 | let mut frames = Vec::new(); 56 | 57 | loop { 58 | match decoder.next_frame() { 59 | Ok(Frame { data, channels, .. }) => { 60 | if channels < 1 { 61 | return Err(Box::from("Invalid number of channels")); 62 | } 63 | 64 | for samples in data.chunks_exact(channels) { 65 | frames.push(f32::from( 66 | samples.iter().fold(0, |sum, x| sum + x / channels as i16), 67 | )); 68 | } 69 | } 70 | Err(minimp3::Error::Eof) => break, 71 | Err(e) => return Err(Box::from(e)), 72 | } 73 | } 74 | 75 | Ok(frames) 76 | } 77 | 78 | /// Find points with max magnitude in each of the bins 79 | fn get_key_points(arr: &[Complex]) -> usize { 80 | let mut high_scores: Vec = vec![0.0; FREQ_BINS.len()]; 81 | let mut record_points: Vec = vec![0; FREQ_BINS.len()]; 82 | 83 | for bin in FREQ_BIN_FIRST..=FREQ_BIN_LAST { 84 | let magnitude = arr[bin].re.hypot(arr[bin].im); 85 | 86 | let mut bin_idx = 0; 87 | while FREQ_BINS[bin_idx] < bin { 88 | bin_idx += 1; 89 | } 90 | 91 | if magnitude > high_scores[bin_idx] { 92 | high_scores[bin_idx] = magnitude; 93 | record_points[bin_idx] = bin; 94 | } 95 | } 96 | 97 | hash(&record_points) 98 | } 99 | 100 | fn hash(arr: &[usize]) -> usize { 101 | (arr[3] - (arr[3] % FUZZ_FACTOR)) * usize::pow(10, 8) 102 | + (arr[2] - (arr[2] % FUZZ_FACTOR)) * usize::pow(10, 5) 103 | + (arr[1] - (arr[1] % FUZZ_FACTOR)) * usize::pow(10, 2) 104 | + (arr[0] - (arr[0] % FUZZ_FACTOR)) 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use super::*; 110 | 111 | #[test] 112 | fn test_hash() { 113 | assert_eq!(hash(&[40, 20, 50, 30]), 3005002040); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/db/pg.rs: -------------------------------------------------------------------------------- 1 | //! `pg` module implements `Repository` with PostgreSQL database and takes care 2 | //! of direct interaction with database. 3 | use crate::db::Repository; 4 | 5 | use postgres::NoTls; 6 | use r2d2_postgres::PostgresConnectionManager; 7 | 8 | use std::cmp::Reverse; 9 | use std::collections::HashMap; 10 | use std::error::Error; 11 | 12 | type PgBigInt = i64; 13 | type PgInteger = i32; 14 | 15 | struct Candidate { 16 | song_id: i32, 17 | match_num: usize, 18 | } 19 | 20 | /// `Table` is used as a counter structure to find the most similar songs in database. 21 | struct Table { 22 | /// highest number of matches among timedelta_best 23 | absolute_best: usize, 24 | /// highest number of matches for every timedelta 25 | timedelta_best: HashMap, 26 | } 27 | 28 | #[allow(dead_code)] 29 | struct Hash { 30 | hid: PgInteger, 31 | hash: PgBigInt, 32 | time: PgInteger, 33 | sid: PgInteger, 34 | } 35 | 36 | /// `PostgresRepo` is an implementation of `Repository` interface. 37 | pub struct PostgresRepo { 38 | pool: r2d2::Pool>, 39 | } 40 | 41 | impl PostgresRepo { 42 | /// Connect to postgres database 43 | pub fn open(config: &str) -> Result { 44 | let manager = PostgresConnectionManager::new(config.parse()?, NoTls); 45 | let pool = r2d2::Pool::new(manager).unwrap(); 46 | Ok(PostgresRepo { pool }) 47 | } 48 | } 49 | 50 | impl Repository for PostgresRepo { 51 | fn index(&self, song: &str, hash_array: &[usize]) -> Result<(), Box> { 52 | let mut conn = self.pool.clone().get().unwrap(); 53 | 54 | let sid: PgInteger = conn 55 | .query( 56 | "INSERT INTO songs(song) VALUES($1) returning sid;", 57 | &[&song], 58 | )? 59 | .get(0) 60 | .unwrap() 61 | .get("sid"); 62 | 63 | let stmt = conn.prepare("INSERT INTO hashes(hash, time, sid) VALUES($1, $2, $3);")?; 64 | for (time, hash) in hash_array.iter().enumerate() { 65 | conn.query(&stmt, &[&(*hash as PgBigInt), &(time as PgInteger), &sid])?; 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | fn find(&self, hash_array: &[usize]) -> Result, Box> { 72 | let mut conn = self.pool.clone().get().unwrap(); 73 | 74 | let mut cnt = HashMap::::new(); 75 | let stmt = conn.prepare("SELECT * FROM hashes WHERE hash=$1;")?; 76 | 77 | for (t, &h) in hash_array.iter().enumerate() { 78 | let result = conn.query(&stmt, &[&(h as PgBigInt)])?; 79 | for row in &result { 80 | let hash_row = Hash { 81 | hid: row.get("hid"), 82 | hash: row.get("hash"), 83 | time: row.get("time"), 84 | sid: row.get("sid"), 85 | }; 86 | 87 | *cnt.entry(hash_row.sid) 88 | .or_insert(Table { 89 | absolute_best: 0, 90 | timedelta_best: HashMap::new(), 91 | }) 92 | .timedelta_best 93 | .entry(hash_row.time - t as i32) 94 | .or_insert(0) += 1; 95 | 96 | if cnt[&(hash_row.sid)].timedelta_best[&(hash_row.time - t as i32)] 97 | > cnt[&hash_row.sid].absolute_best 98 | { 99 | cnt.get_mut(&hash_row.sid).unwrap().absolute_best = 100 | cnt[&hash_row.sid].timedelta_best[&(hash_row.time - t as i32)] 101 | } 102 | } 103 | } 104 | 105 | if cnt.is_empty() { 106 | return Ok(None); 107 | } 108 | 109 | let mut matchings = Vec::::new(); 110 | for (song, table) in cnt { 111 | matchings.push(Candidate { 112 | song_id: song, 113 | match_num: table.absolute_best, 114 | }); 115 | } 116 | 117 | matchings.sort_by_key(|a| Reverse(a.match_num)); 118 | 119 | let song_name: String = conn 120 | .query( 121 | "SELECT song FROM songs WHERE sid=$1;", 122 | &[&matchings[0].song_id], 123 | )? 124 | .get(0) 125 | .unwrap() 126 | .get("song"); 127 | let similarity = (100.0 * matchings[0].match_num as f64 / hash_array.len() as f64) as isize; 128 | Ok(Some(format!("{} ({}% matched)", song_name, similarity))) 129 | } 130 | 131 | fn delete(&self, song: &str) -> Result> { 132 | let mut conn = self.pool.clone().get().unwrap(); 133 | match conn.execute("DELETE FROM songs WHERE song=$1;", &[&song]) { 134 | Ok(affected) => Ok(affected), 135 | Err(e) => Err(Box::from(e)), 136 | } 137 | } 138 | } 139 | --------------------------------------------------------------------------------