Compare commits

...

15 Commits

Author SHA1 Message Date
f42985733f Add some varied default song covers 2023-09-24 01:33:55 +02:00
c6cc83018f Dockerfile from scratch 2023-09-24 01:32:46 +02:00
89cb0c475b 1.0.0 2023-09-23 23:51:07 +02:00
95dbb74b47 Add backend 2023-09-23 18:23:46 +02:00
a894717e52 Update dependencies 2023-09-23 16:45:16 +02:00
1092860b42 Add support for custom sublists 2023-09-23 15:44:36 +02:00
ec5daba300 Fix typo 2022-04-19 15:35:37 +02:00
3800b448e1 0.2.2 2022-04-19 15:15:00 +02:00
4a6bb957a1 Update dockerfile & add .dockerignore 2022-04-19 15:14:40 +02:00
666f87a5f3 Improve searching and search hints 2022-04-19 15:10:24 +02:00
06e12c186a Add Dockerfile 2022-02-18 23:18:41 +01:00
e0d0bef292 Add search suggestions 2022-01-05 16:05:54 +01:00
004dd7d161 Add background image 2022-01-05 12:19:36 +01:00
dc5460af0d Improve search performance 2022-01-05 11:58:08 +01:00
37a7cbba6e 0.2.1 2022-01-04 15:39:57 +01:00
73 changed files with 7765 additions and 649 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
Dockerfile
target

5
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/dist
target
dist
.env

1855
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,3 @@
[package]
name = "singit2"
version = "0.2.0"
authors = ["Joakim Hulthe <joakim@hulthe.net"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
seed = "0.8.0"
#wasm-bindgen = "0.2.70"
serde = { version = "1", features = ['derive'] }
serde_json = "1"
anyhow = "*"
rand = "0.8.4"
[dependencies.css_typegen]
git = "https://github.com/hulthe/css_typegen.git"
branch = "master"
[workspace]
members = ["backend", "frontend"]
resolver = "2"

76
Dockerfile Normal file
View File

@ -0,0 +1,76 @@
##################
### BASE STAGE ###
##################
FROM rust:1.72.1 as base
# Install build dependencies
RUN cargo install --locked trunk strip_cargo_version
RUN rustup target add wasm32-unknown-unknown
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
RUN mkdir frontend backend common
###########################
### STRIP-VERSION STAGE ###
###########################
FROM base AS strip-version
COPY Cargo.lock Cargo.toml ./
COPY frontend/Cargo.toml ./frontend/
COPY backend/Cargo.toml ./backend/
#COPY common/Cargo.toml ./common/
RUN strip_cargo_version
###################
### BUILD STAGE ###
###################
FROM base AS build
RUN cargo init --lib frontend
RUN cargo init --bin backend
RUN cargo init --lib common
COPY --from=strip-version /app/frontend/Cargo.toml /app/frontend/
COPY --from=strip-version /app/backend/Cargo.toml /app/backend/
#COPY --from=strip-version /app/common/Cargo.toml /app/common/
COPY --from=strip-version /app/Cargo.toml /app/Cargo.lock /app/
WORKDIR /app/backend
RUN cargo build --release --target x86_64-unknown-linux-musl
WORKDIR /app/frontend
RUN cargo build --release --target wasm32-unknown-unknown
WORKDIR /app
COPY . .
WORKDIR /app/backend
RUN cargo build --release --target x86_64-unknown-linux-musl
WORKDIR /app/frontend
RUN trunk build --release
########################
### PRODUCTION STAGE ###
########################
FROM scratch
# Default logging level
ENV RUST_LOG="info"
ENV COVERS_DIR="/covers"
VOLUME /covers
WORKDIR /
# Copy application binary
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/singit_srv /usr/local/bin/singit_srv
# Copy static web files
COPY --from=build /app/frontend/dist /dist
# Copy database migrations
COPY backend/migrations /migrations
ENTRYPOINT ["singit_srv"]

1
backend/.env.example Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres

2014
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
backend/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "singit_srv"
version = "1.0.0"
authors = ["Joakim Hulthe <joakim@hulthe.net"]
edition = "2021"
[dependencies]
actix-files = "0.6.2"
actix-web = { version = "4.4.0", default-features = false, features = ["macros"] }
clap = { version = "4.4.4", features = ["derive", "env"] }
diesel = "2.1.1"
diesel-async = { version = "0.4.1", features = ["postgres", "deadpool"] }
dotenv = "0.15.0"
env_logger = "0.10.0"
eyre = "0.6.8"
log = "0.4.20"
serde = { version = "1.0.188", default-features = false, features = ["derive", "std"] }

9
backend/diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

1
backend/dist Symbolic link
View File

@ -0,0 +1 @@
../frontend/dist

0
backend/migrations/.keep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,2 @@
DROP TABLE song;

View File

@ -0,0 +1,13 @@
CREATE TABLE song(
song_hash TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
artist TEXT NOT NULL,
cover TEXT,
language TEXT,
video TEXT,
year TEXT,
genre TEXT,
bpm TEXT NOT NULL,
duet_singer_1 TEXT,
duet_singer_2 TEXT
);

View File

@ -0,0 +1,2 @@
DROP TABLE custom_list_entry;
DROP TABLE custom_list;

View File

@ -0,0 +1,10 @@
CREATE TABLE custom_list (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE custom_list_entry (
list_id SERIAL REFERENCES custom_list(id),
song_hash TEXT NOT NULL REFERENCES song(song_hash),
PRIMARY KEY (list_id, song_hash)
);

View File

@ -0,0 +1,253 @@
insert into custom_list(id, name) VALUES (1, 'tux');
insert into custom_list_entry(list_id, song_hash) VALUES
(1, '415e7f2a9ca15306f462493dea011328'),
(1, 'c2ada59d4465e891565ed6480c95402b'),
(1, 'b64e1a6f02bc3dbc9cf42ec51465f8a4'),
(1, '61611e2ae7da9f5fa307c677aa768749'),
(1, '331b7741323bb299558d2889662bc90c'),
(1, '83c8eb8a644c01085406611a54c746b3'),
(1, '5305707bae7a4072d6f72d2c83c6fe19'),
(1, '2a405748c627c30bfdf029361d09618c'),
(1, '24bf0d992eb9bfe83f0355574e52cd66'),
(1, '128d3a0b6109b61e7c5266942e04e45a'),
(1, '4cda857d6a4d1365a98f0f4b0f6292da'),
(1, '8fd5f78019ef7cdfa595d5c51a147ba7'),
(1, '138a95285762edcfb7916b9aef329083'),
(1, '8e9c0d935b93569858817a4ae9513875'),
(1, 'b865f24ced688b6932c6131d644f7975'),
(1, 'f83893a4322289818328b42468728314'),
(1, '92d04dbea98658cd07d9b2c29356ab52'),
(1, '9e2975f1cb67bd0ff76c83e9d2b3c7a4'),
(1, 'cf9eaf021b3d78cd2491be771abbe13b'),
(1, '3a04e1eec93c224af777c7f951dd2d98'),
(1, 'b20b9c8f2bdb43086ec9d46b9a6bada0'),
(1, '85ad23ce6d40d27656a9cbd54e3f6ab4'),
(1, 'ab32d01ba3d2a557c51c7b26f08f61f0'),
(1, '989118dd28898b84962858ec44e2f7e2'),
(1, 'bf7a666e3d37a1d7388ed838bb6f020a'),
(1, 'd30cf94ad68c50f660a31bcf637c76bd'),
(1, '053161d0a98c27ea4a9ee5e0e404e848'),
(1, 'c9cf0764a339f79b783087db605fe8f0'),
(1, 'd5c4c3513cb8869643f42e1350474ad3'),
(1, '0a8a99dc8ffafaf837f0755fa2e54009'),
(1, 'fcea59940c64e1257ce9264254ba8e03'),
(1, '63c18435bbeef45e6fafc947ca5b1ff5'),
(1, '6620e5b50f4f1ee7358bbe0d609edfe2'),
(1, '265a22dc97ee56e2e1a4ce525f3e79c2'),
(1, '9adb1f7ec660e0f2643cd151fad69b82'),
(1, '6443a7047288aab14ee4694ebe87b79b'),
(1, '80a356c273b7d52049a23e91e36aa9f0'),
(1, '0f73fa30f7f20d5235dcac8eabff472d'),
(1, '5a4b69e047a949744a81715a83f20ee3'),
(1, '3407c6f6ee7b1970c094b0c3144bf54a'),
(1, '9a4fbaee92151f345680abe2fc9ee747'),
(1, '1a049f2fe3a59d840c5157600c8743b5'),
(1, 'd6f4b7dd36e9ddb4e654d2d6836ff15e'),
(1, 'be090ec6b8d3dcd206c29c2620b673d6'),
(1, '2de6e630dc402b3f1dd565e25376fa6e'),
(1, '8527cc85dfb71405e0c509d60d530fa1'),
(1, '495b964e442823eb695e6e73d59ed100'),
(1, '09bfeb3ac6fc284b0ef8ab47c055e2b0'),
(1, 'a5f09a3e3aae08d5576d8c24656ea1da'),
(1, 'e7110b6577a733df0ef96559c72fdc3a'),
(1, '61a8dacb968a51d010af72cd9ba0fcc7'),
(1, '126c7aa03977ba6179c14d1286d95987'),
(1, '7bafa94e8b192112fcc13285733fed21'),
(1, '55efe418039d6b4c1a0a3033a505a6d5'),
(1, 'd6cd321817e8d2ed7ebb2a7282d7ba08'),
(1, '56f7f540f406a8f4feabdc976ffc02a5'),
(1, '5bd19f7edc9a5aceb165bf982777fbef'),
(1, 'e8f3382284dd8f3c14f74e543cbe1365'),
(1, '8b1bef88f60a945f23695a98ab461a32'),
(1, '34340558fe5254ec8d632b28412c1df6'),
(1, '133c04455080e1e700ea9168eb6e9154'),
(1, '40b939fea1cc7bca69f94ba98a9a8966'),
(1, '17110ec3c9490c2d7212bac578178f82'),
(1, '4013a6b73babbe4ddb7c94308f607981'),
(1, '606aa3c2fd587ba49e06e2ba96634d3e'),
(1, 'f3ddf171f30f886bb65f2eb9b5e9b631'),
(1, 'b441c2178c3ea7b470905972a8542a45'),
(1, '8c0bcc859b936e07125a8a1496fc5cff'),
(1, '37903e02cd6f096b7c18130ef322f245'),
(1, '246a6ba918b31591b8a3bb39e89e57ca'),
(1, 'df864a07605f68f58d454991035887ad'),
(1, '4407108e5fcce7999b4ed6a02235829e'),
(1, '8504e0862ec12cca41caa1f78681ec03'),
(1, '75a3f6fbf821d473bf17025e043a5964'),
(1, '0bc6e109374f04db38ec70d292371d19'),
(1, '5fb96e336c571545a9e3a1afa0ce25c2'),
(1, 'def1a11db7a5899c9b0ba357fc71a37f'),
(1, '581e463cee62d8adafd84b13deeb507d'),
(1, '503a857d480a4ee4c30b132b80e895d1'),
(1, 'cf60d6a32e7a1256f1e69b518fe158ff'),
(1, '980817c5a574eb50f3ab26f390b84b4f'),
(1, '2aa8f43167062239fa955a6fd5bf209b'),
(1, '16318fbae71adefaf80991849a181d11'),
(1, '43fd446c68528a56931f755525b8bafc'),
(1, 'abf514722acd55a48f5c1ec8badd899d'),
(1, 'fb3dd61449de05c2b84d3d75cf346d81'),
(1, '55a069e51045adab0adf4d81ca36011e'),
(1, '5694db971c54e0872defef4a300343f9'),
(1, 'd67f527b382394095a3ca40ac8449904'),
(1, 'db52ed089f2008181515e3f8be54bf90'),
(1, 'f2fffd34cef9a1878754640688487c82'),
(1, 'd6ddd5009d865979febbfb36bc2d99b7'),
(1, '362c2a15b7e8aaf8f1dfefd717fe7e29'),
(1, 'b2c13e3d09706d5fde0243a44058f81c'),
(1, '37d139647cca8827ad1205695a3daa33'),
(1, 'c5732364b28b208d22e0f4cab71b5907'),
(1, '236197f68795dea908d97fa61d8bb1c5'),
(1, 'f54e7bc914d7c5fab66b371399acfd41'),
(1, 'c2563f5a17677676ec45a0d487a2a871'),
(1, '08f30c15ce2135cdf575e71aa9fadee8'),
(1, 'ebd1f487cdb42b7d444b23c68a79918b'),
(1, 'ae2739faa6e4b4c9de6962071be9fec6'),
(1, '29c04079f0a8cdd22d95ff605847ccce'),
(1, '3a0ffffb65779005d55f8d8f3d0a7091'),
(1, '14c06f589bb0a772387888641b52454b'),
(1, 'e0f3f3e0afff4e71d8a2e3a959a8effa'),
(1, '2b843c3751dde9cfadfd392cac900487'),
(1, '8c876d8344b468cfb89a5cc91c4a5218'),
(1, '15917f7b815dce661db415ee1c488813'),
(1, '54fb8b43f54e36e9a6a546d3c45aa4cf'),
(1, 'a9430cf8d75c891c6c27ca6750faee8e'),
(1, 'eb50f5eee7f570e976ab423fb44edde9'),
(1, 'ed48d149f7ffa48d8c7eed5270f48949'),
(1, '6fa37a33a1baea4875c9e342f8091dc3'),
(1, '9ce446680bea61864df9d9fec5d54a93'),
(1, 'f7ea025406d2645f86da52dde64807d0'),
(1, '114fb59b3fee8ebf8c36fd2bf7e59a55'),
(1, 'c1729df7dc9452a496c7be7f043ce88d'),
(1, '92db823664bfa36f7fdde309fe69eb26'),
(1, 'df833bc0ae6fd919e0db280a8f0f5b9e'),
(1, '7d88370d2be9063ef347351781e3ad38'),
(1, '1dbcab635439ff5d3432407fedb94da1'),
(1, 'c79b580b5ba99bdf4669a61379747740'),
(1, 'ce3722d28640ab845396d37933c66b1a'),
(1, 'dd55c007590f65a011febd5c5ec17262'),
(1, 'bb51d1b9fb2ca91fab7e63066f797c97'),
(1, '597e0dc97c39604cbac73a887f89bf94'),
(1, '2ed7517269cece052f7cbe4d8ec6f50c'),
(1, '9c4adad2919051e6fe586354b9af132b'),
(1, '2f9cc67c261ee31a0ecdd154168a987a'),
(1, 'bb1a82b02f0de5a90fcb44edd2cccdf9'),
(1, '9ec7df7febfce66dc43941e2c259904b'),
(1, '1fd38f24222630b30cca1978e2acdb85'),
(1, '9d051739db959a5dfb5b98890b4e6ae6'),
(1, '9ae26319e307759e6e27c85ddff8f5fa'),
(1, '3e3ac2c042b18fcc7f9610f654778740'),
(1, '92aeb6900e0e06603dd846889c5d8ded'),
(1, '096d2a19ddf0911a66c4fe4b41f992c7'),
(1, 'cde2d982ab2caee5f0715c341bcae991'),
(1, 'aa545b027a6fa39e5c278fc366ad9016'),
(1, '1c5b8e85a66a415e39d996b967f92e67'),
(1, '1b4673e2376ecfa02bae442cdc5b3c90'),
(1, '9e542ff1cb9fb17f5f7a60af1fe9e66b'),
(1, '8aa0b348759b2c56e17074b65a50f04a'),
(1, '4931f242aeb4b1b641d813c6a2fbfadd'),
(1, '2e3cfa5057486d0759cee8ee477a1678'),
(1, '40444256345769c7a911e8710f0f5733'),
(1, '54f85dd1796f5a8f5797c3965e8736f4'),
(1, 'd8b2250234456105c3f1452afcffbfd5'),
(1, '038e1f5299c2e7c4c87b54f655429666'),
(1, '2ba74b69dc4671aa0fa705306c7cf072'),
(1, 'fef173c794fec61b65089321673d0370'),
(1, '366eff873b9665852f08b3d134dfd87c'),
(1, '1bdc4632029c88612f481ab75ebc4062'),
(1, 'b3acb1a516ee6949dd0f764200e80287'),
(1, '00d59e67b764386960d8b423567cf9bc'),
(1, 'b78fa5394c788492c5ddcfdbdfe4ae15'),
(1, 'e8a7d9cb2aca242bf3f0c59640ddd620'),
(1, '6059a22be14860cff767d679c98ed1df'),
(1, 'b23ce25c9b6d789953b43d6a7f6f9266'),
(1, '0499534b7a0545e81354fd6e814f4ad3'),
(1, '77ea72cb592979d8aa617f1a06181600'),
(1, '482de10e7a8d0688dd9ac27eef4cddb8'),
(1, '570b9443265e8614befdb3c84bf66095'),
(1, '5f572916554e6053ee9a59c2fbadb7c7'),
(1, '79a9316e23134dc7632c51ec305af756'),
(1, '4656a71e7802cff7757cc3e4a9281927'),
(1, 'f986b0490311d17c909d8b8cceb6756d'),
(1, '11c183d403657cafd9fe8f22921686cc'),
(1, 'cbbf9ec130e723aa24e600d16471570b'),
(1, 'e9a45f7858204bfc7b9ae196c3d9d7a7'),
(1, '47097d94df0cb3b5f4c281e4d1077ce5'),
(1, '09241a765e64d1f2280a888f255add7a'),
(1, 'fb8f0d2dd52a205aa367d3f1b549f9c1'),
(1, '97f260fceebdedcdc9f5ab99fef8a78e'),
(1, '86a91d11239bc96df9c917c99bb19050'),
(1, 'a1bac8f28c3378e56d0485cc880098d2'),
(1, 'f563a6a7d1a20fc4dddca1dab785bd1f'),
(1, '49159657f8644de0ea1c2dca29b11482'),
(1, 'd94065d725bbdeedeebe1c5a8693873f'),
(1, '03563f756de4a637b2bfa6c33b9ba3cd'),
(1, '97b51c9df42b8e8754a6127073b6cf82'),
(1, '865a527fc94bf609d2a86e6e7c2ca481'),
(1, 'cb877f34c2a65534a458bf54ea823f08'),
(1, '9d3b3d092d27b3ed0b76281225f98bf7'),
(1, 'b27e8ad3b0d538abe256169cb3363b4a'),
(1, '51c3819eb7ced02c46c22acda2c74205'),
(1, '7ba6a2f608295d678aea4195b46a0219'),
(1, 'db9785d69f04ab00266814b8e992da91'),
(1, '2db8d58d0f3062f9d7e1259492652c1d'),
(1, 'e2065c855899f8ed49e6ce89162683e8'),
(1, '1319c9749c2ffdc6832e5aa98b54e659'),
(1, 'fd34b1095c30bde6ce7aab8ec0683bb7'),
(1, '7ad81d348b958714dea07ece9fd522c2'),
(1, '2fb87de8954d03f91e6771846e2a8d6f'),
(1, '6597cb67e75e5991ceb2afe448db3c35'),
(1, '6c2619df4ae1e9377d47116d88eeeb5d'),
(1, 'd25074818bd0a543482f55ff21330676'),
(1, '8adf0a8c2be11dac6ec490bfebcc8d30'),
(1, 'b1b6e78d3e26e1aed5a408ddb975547b'),
(1, '1c8437de40bafcd6ba379ac53f1b8169'),
(1, 'bb03494e04ca2cd3918de70f74498fcf'),
(1, 'e00752cbe957fb707c6a6ac574452cd5'),
(1, 'e1360e0256b6ce58c3f8591bb93a70dc'),
(1, 'fd6c48fc6006b8276d12f8859daec350'),
(1, '75b513f7b143b20a53c9a6d415bb02d1'),
(1, 'ed2eff479f6e1333c83561125f8e64b9'),
(1, '90a269caf890d1fae8fe57dc4ed19e56'),
(1, 'b553b566dbb94a712641f346db436ad4'),
(1, '76f9f24634d3590cbdc3dbfc2d84cb6c'),
(1, '30c94206e5aac9e6936c42a2f60cc426'),
(1, '692915e5a20c90c4c0d73f3db9c1154a'),
(1, '836c2fa67a8d62c3b8c04bccd2ae31be'),
(1, 'a514416bfa9ec0a46b98de23194398a4'),
(1, '6abd9038780dd1f005df9fc31feb1560'),
(1, 'e35237b0b1595f75b6e641669d67cfce'),
(1, 'cbd15cc24b2892beb3444780fdbb19be'),
(1, '26c91da37713481245b08bd74565f06b'),
(1, '95cbbc0c9529592bee188dd2f3e7b8c7'),
(1, 'f5d7f236ed25c850ec5e2f462eed7c4f'),
(1, 'e5d7e266169ff20581413412b970c66c'),
(1, 'e1e60351f1d5c9b62ad373e6343ccb78'),
(1, '8c4a8899f75122dece210f306fc50de7'),
(1, '70ec05034e36531f8cc15a995ee2a1ca'),
(1, '1a9f9cbdf1adedcba49592fb48fb23f1'),
(1, '1369618c25eb074761f2f9dd12cd082e'),
(1, 'bcdd94a0c273aa1b0700de7e5e4213d0'),
(1, '65248f0a7165ab82ee03e312f20526e7'),
(1, '4c494372395a62e213ebecfe4dff643b'),
(1, 'c270582f05844d3adef31a226386100f'),
(1, '97047972139aa66b73ad3af35ca45b44'),
(1, '40242bfc27f4b1a1af097bf88cc9619d'),
(1, '604cabd06bd3b31fc5b8dd3802fcde2c'),
(1, '687dc02afb37f19b29ceb2339eb52676'),
(1, '581294986a59b3403d05de69c6be96d7'),
(1, '38ec3bbef46ad213aef4731fe049f5f2'),
(1, 'b50c594fc7e5603d5178b33109f6fb02'),
(1, '7988cb4fd66dd5acf3c8e4ed87a1ec5f'),
(1, '53a805ec4780ff7641acdfe374373e80'),
(1, '226b3556a1804e38029fd237a66b1db0'),
(1, 'b36725f672acde360df83bb877028d73'),
(1, '6ba01a6240c18488040f9385de557081'),
(1, '3b35edddcab0cac1aec5615dc86cd1f8'),
(1, '8a9d0cf19bd5111f45b75ac99ef298e8'),
(1, '1fa8e36ea62adfd1dbe8943a5cbd7457'),
(1, '38304b8fd2f1552ee09b14be3c1d3f2e'),
(1, 'acc5ce8a0c88cb4c5f74140e36f83e1f'),
(1, '948fb19cc466d95cd9fc7188ad5411bf'),
(1, 'c5024d345c3453b92b627bfc212dbf0e'),
(1, 'ef5ca491b2c6298026a4eb425ec63c6b'),
(1, '9b661821b671d44f7221a4a36ab80d8d');

2144
backend/mock/songs.sql Normal file

File diff suppressed because it is too large Load Diff

23
backend/src/db.rs Normal file
View File

@ -0,0 +1,23 @@
use diesel_async::{
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
AsyncPgConnection,
};
use log::info;
use crate::Opt;
pub type DbPool = Pool<AsyncPgConnection>;
pub async fn setup(opt: &Opt) -> eyre::Result<DbPool> {
let manager = AsyncDieselConnectionManager::new(&opt.database_url);
info!("setting up database pool");
let pool = Pool::builder(manager).build()?;
info!("testing database connection");
let _db_test = pool.get().await?;
// TODO: migrations
Ok(pool)
}

178
backend/src/main.rs Normal file
View File

@ -0,0 +1,178 @@
mod db;
mod schema;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use actix_files::NamedFile;
use actix_web::{
get,
middleware::Logger,
web::{self, Json},
App, HttpRequest, HttpServer, Responder,
};
use clap::Parser;
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl;
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use crate::db::DbPool;
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Queryable,
Selectable,
)]
#[diesel(table_name = crate::schema::song)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Song {
pub song_hash: String,
pub title: String,
pub artist: String,
pub cover: Option<String>,
pub language: Option<String>,
pub video: Option<String>,
pub year: Option<String>,
pub genre: Option<String>,
pub bpm: String,
#[serde(rename = "duetsingerp1")]
pub duet_singer_1: Option<String>,
#[serde(rename = "duetsingerp2")]
pub duet_singer_2: Option<String>,
}
#[derive(Serialize, Deserialize, Queryable, Selectable, Debug, Clone, Default)]
#[diesel(table_name = crate::schema::custom_list)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct CustomList {
id: i32,
name: String,
}
#[get("/")]
async fn root() -> actix_web::Result<NamedFile> {
let path: &Path = "dist/index.html".as_ref();
Ok(NamedFile::open(path)?)
}
async fn index(req: HttpRequest) -> actix_web::Result<NamedFile> {
let path: PathBuf = req.match_info().query("filename").parse().unwrap();
let path = Path::new("dist").join(path);
Ok(NamedFile::open(path)?)
}
#[get("/images/songs/{image}")]
async fn song_image(
path: web::Path<String>,
opt: web::Data<Arc<Opt>>,
) -> actix_web::Result<NamedFile> {
let image = path.into_inner();
let path = opt.covers_dir.join(image);
Ok(NamedFile::open(path)?)
}
#[get("/songs")]
async fn songs(pool: web::Data<DbPool>) -> impl Responder {
use schema::song::dsl::*;
let mut db = pool.get().await.unwrap();
let songs = song.select(Song::as_select()).load(&mut db).await.unwrap();
Json(songs)
}
#[get("/custom/lists")]
async fn custom_lists(pool: web::Data<DbPool>) -> impl Responder {
use schema::custom_list::dsl::*;
let mut db = pool.get().await.unwrap();
let lists: Vec<String> = custom_list.select(name).load(&mut db).await.unwrap();
Json(lists)
}
#[get("/custom/list/{list}")]
async fn custom_list(pool: web::Data<DbPool>, path: web::Path<String>) -> impl Responder {
use schema::custom_list::dsl::*;
use schema::custom_list_entry::dsl::*;
let mut db = pool.get().await.unwrap();
let list: CustomList = custom_list
.select(CustomList::as_select())
.filter(name.eq(&*path))
.get_result(&mut db)
.await
.unwrap();
let list_entries: Vec<String> = custom_list_entry
.select(song_hash)
.filter(list_id.eq(list.id))
.load(&mut db)
.await
.unwrap();
Json(list_entries)
}
#[derive(Parser)]
pub struct Opt {
/// Address to bind to.
#[clap(short, long, env = "BIND_ADDRESS", default_value = "0.0.0.0")]
address: String,
/// Port to bind to.
#[clap(short, long, env = "BIND_PORT", default_value = "8080")]
port: u16,
/// Postgresql URL.
#[clap(short, long, env = "DATABASE_URL")]
database_url: String,
/// Directory where song covers are stored.
#[clap(short, long, env = "COVERS_DIR")]
covers_dir: PathBuf,
}
#[actix_web::main]
async fn main() -> eyre::Result<()> {
dotenv().ok();
let opt = Arc::new(Opt::parse());
env_logger::init();
let db_pool = db::setup(&opt).await?;
let app = {
let opt = Arc::clone(&opt);
move || {
let logger = Logger::default();
App::new()
.wrap(logger)
.app_data(web::Data::new(db_pool.clone()))
.app_data(web::Data::new(Arc::clone(&opt)))
.service(root)
.service(songs)
.service(song_image)
.service(custom_list)
.service(custom_lists)
.route("/{filename:.*}", web::get().to(index))
}
};
HttpServer::new(app)
.bind((opt.address.as_str(), opt.port))?
.run()
.await?;
Ok(())
}

36
backend/src/schema.rs Normal file
View File

@ -0,0 +1,36 @@
// @generated automatically by Diesel CLI.
diesel::table! {
custom_list (id) {
id -> Int4,
name -> Text,
}
}
diesel::table! {
custom_list_entry (list_id, song_hash) {
list_id -> Int4,
song_hash -> Text,
}
}
diesel::table! {
song (song_hash) {
song_hash -> Text,
title -> Text,
artist -> Text,
cover -> Nullable<Text>,
language -> Nullable<Text>,
video -> Nullable<Text>,
year -> Nullable<Text>,
genre -> Nullable<Text>,
bpm -> Text,
duet_singer_1 -> Nullable<Text>,
duet_singer_2 -> Nullable<Text>,
}
}
diesel::joinable!(custom_list_entry -> custom_list (list_id));
diesel::joinable!(custom_list_entry -> song (song_hash));
diesel::allow_tables_to_appear_in_same_query!(custom_list, custom_list_entry, song,);

687
frontend/Cargo.lock generated Normal file
View File

@ -0,0 +1,687 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "css_typegen"
version = "0.2.0"
source = "git+https://github.com/hulthe/css_typegen.git?branch=master#ed1c4b1b7c8bf19e1ce045cafa12f3886041134c"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
]
[[package]]
name = "csv"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "enclose"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
[[package]]
name = "futures-task"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
[[package]]
name = "futures-util"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gloo-console"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
dependencies = [
"gloo-utils 0.2.0",
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-events"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [
"futures-channel",
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils 0.2.0",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "http"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "pin-project"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "seed"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c0e296ea0569d20467e9a1df3cb6ed66ce3b791a7eaf1e1110ae231f75e2b46"
dependencies = [
"enclose",
"futures",
"getrandom",
"gloo-file",
"gloo-timers",
"gloo-utils 0.1.7",
"indexmap",
"js-sys",
"rand",
"uuid",
"version_check",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "serde_json"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "singit2"
version = "0.2.2"
dependencies = [
"css_typegen",
"csv",
"gloo-console",
"gloo-net",
"rand",
"seed",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "uuid"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.37",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "web-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
]

20
frontend/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "singit_web"
version = "1.0.0"
authors = ["Joakim Hulthe <joakim@hulthe.net"]
edition = "2021"
[dependencies]
seed = "0.10.0"
serde = { version = "1.0.0", features = ["derive"] }
serde_json = "1.0.0"
rand = "0.8.5"
gloo-console = "0.3.0"
gloo-net = "0.4.0"
csv = "1.2.2"
thiserror = "1.0.48"
wasm-bindgen = "0.2.87"
[dependencies.css_typegen]
git = "https://github.com/hulthe/css_typegen.git"
branch = "master"

26
frontend/build.rs Normal file
View File

@ -0,0 +1,26 @@
use std::fs;
use std::path::Path;
use std::io;
fn main() {
index_default_covers().expect("index default covers");
}
/// Index all pngs in static/images/default_covers and expose the list in the build as an
/// environment variable DEFAULT_SONG_COVERS. This list includes the path to the images so the
/// frontend can fetch them.
fn index_default_covers() -> io::Result<()> {
let mut files = vec![];
for dir in fs::read_dir("static/images/default_covers")? {
let path = dir?.path();
if path.extension().and_then(|s| s.to_str()) == Some("png") {
let path = Path::new("/").join(path.strip_prefix("static").unwrap());
files.push(path.to_string_lossy().to_string());
}
}
let list = files.join(",");
println!("cargo:rustc-env=DEFAULT_SONG_COVERS={list}");
Ok(())
}

403
frontend/src/app.rs Normal file
View File

@ -0,0 +1,403 @@
use crate::css::C;
use crate::custom_list::{fetch_custom_song_list, fetch_custom_song_list_index, CustomLists};
use crate::fetch::fetch_list_of;
use crate::fuzzy::FuzzyScore;
use crate::query::ParsedQuery;
use crate::song::Song;
use gloo_console::error;
use rand::seq::SliceRandom;
use rand::thread_rng;
use seed::app::cmds::timeout;
use seed::browser::util::document;
use seed::prelude::*;
use seed::{attrs, button, div, empty, img, input, p, span, C, IF};
use std::cmp::Reverse;
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use web_sys::Element;
pub struct Model {
songs: Vec<(Reverse<FuzzyScore>, Song)>,
/// Custom song lists, lazily loaded.
custom_lists: CustomLists,
/// The search string.
query: String,
/// The number of songs currently in the dom. Goes up when the user scrolls down.
shown_songs: usize,
/// The number of songs that didn't match the search critera.
hidden_songs: usize,
/// Whether we're filtering by video.
filter_video: bool,
/// Whether we're filtering by duets.
filter_duets: bool,
query_placeholder: String,
query_placeholder_len: usize,
autotyper: Option<CmdHandle>,
/// URLs of some defaults for songs with missing cover art.
default_song_covers: Vec<&'static str>,
}
#[derive(Default)]
pub enum Loading<T> {
/// The resource has not started loading.
#[default]
NotLoaded,
/// The resource is currently loading.
InProgress,
/// The resource has loaded.
Loaded(T),
}
const SCROLL_THRESHOLD: usize = 50;
const INITIAL_ELEM_COUNT: usize = 100;
pub enum Msg {
/// Loaded songs.
Songs(Vec<Song>),
/// Loaded custom song index.
CustomSongLists(Vec<String>),
/// Loaded custom song list.
CustomSongList {
list: String,
song_hashes: HashSet<String>,
},
/// The user entered something into the search field
Search(String),
/// The user pressed the Toggle Video button
ToggleVideo,
/// The user pressed the Toggle Duets button
ToggleDuets,
/// The user pressed the Shuffle button
Shuffle,
/// The user scrolled the song list
Scroll,
/// Type stuff in the search input placeholder
Autotyper,
}
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.perform_cmd(fetch_songs());
orders.perform_cmd(fetch_custom_song_list_index());
// get list of default song covers. see build.rs
const DEFAULT_SONG_COVERS: &str = env!("DEFAULT_SONG_COVERS");
let default_song_covers = DEFAULT_SONG_COVERS.split(',').collect();
Model {
songs: vec![],
custom_lists: Default::default(),
query: String::new(),
hidden_songs: 0,
shown_songs: INITIAL_ELEM_COUNT,
filter_video: false,
filter_duets: false,
query_placeholder: String::from("Sök"),
query_placeholder_len: 0,
autotyper: Some(orders.perform_cmd_with_handle(timeout(500, || Msg::Autotyper))),
default_song_covers,
}
}
fn update_song_list(model: &mut Model, orders: &mut impl Orders<Msg>) {
model.hidden_songs = 0;
model.shown_songs = INITIAL_ELEM_COUNT;
scroll_to_top();
if model.query.is_empty() {
model.filter_duets = false;
model.filter_video = false;
update(Msg::Shuffle, model, orders);
} else {
let query = ParsedQuery::parse(&model.query);
model.filter_duets = query.duet == Some(true);
model.filter_video = query.video == Some(true);
if let Some(name) = query.list {
if let Some(l @ Loading::NotLoaded) = model.custom_lists.get_mut(name) {
orders.perform_cmd(fetch_custom_song_list(name.to_string()));
*l = Loading::InProgress;
}
}
// calculate search scores & sort list
for (score, song) in model.songs.iter_mut() {
let new_score = song.fuzzy_compare(&query, &model.custom_lists);
if new_score < Default::default() {
model.hidden_songs += 1;
}
*score = Reverse(new_score);
}
model.songs.sort_unstable();
}
}
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Songs(songs) => {
model.songs = songs
.into_iter()
.map(|song| (Default::default(), song))
.collect();
}
Msg::CustomSongLists(lists) => {
model.custom_lists = lists
.into_iter()
.map(|list| (list, Loading::NotLoaded))
.collect();
}
Msg::CustomSongList { list, song_hashes } => {
let query = ParsedQuery::parse(&model.query);
let update_list = query.list == Some(&list);
*model.custom_lists.entry(list).or_default() = Loading::Loaded(song_hashes);
if update_list {
update_song_list(model, orders);
}
}
Msg::Search(query) => {
model.query = query;
update_song_list(model, orders);
}
Msg::ToggleVideo => {
let mut query = ParsedQuery::parse(&model.query);
query.video = match query.video {
Some(true) => None,
None | Some(false) => Some(true),
};
update(Msg::Search(query.to_string()), model, orders);
}
Msg::ToggleDuets => {
let mut query = ParsedQuery::parse(&model.query);
query.duet = match query.duet {
Some(true) => None,
None | Some(false) => Some(true),
};
update(Msg::Search(query.to_string()), model, orders);
}
Msg::Shuffle => {
model.hidden_songs = 0;
model.shown_songs = INITIAL_ELEM_COUNT;
scroll_to_top();
model.query.clear();
model.songs.shuffle(&mut thread_rng());
autotype_song(model, orders);
}
Msg::Scroll => {
let Some((scroll, max_scroll)) = get_scroll() else {
error!("Failed to get song list element by id:", SONG_LIST_ID);
return;
};
let scroll_left: i32 = max_scroll - scroll;
// when there are fewer elements than this below the scroll viewport, add more
const ELEMENT_HEIGHT: i32 = 48;
if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 {
model.shown_songs += 1;
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
}
}
Msg::Autotyper => {
model.query_placeholder_len += 1;
while !model
.query_placeholder
.is_char_boundary(model.query_placeholder_len)
{
model.query_placeholder_len += 1;
}
if model.query_placeholder_len < model.query_placeholder.len() {
model.autotyper =
Some(orders.perform_cmd_with_handle(timeout(80, || Msg::Autotyper)));
}
}
}
}
pub fn view(model: &Model) -> Vec<Node<Msg>> {
let song_card = |song: &Song| -> Node<Msg> {
div![
C![C.song_item],
img![
C![C.song_item_cover],
match song.cover {
Some(_) => attrs! {At::Src => format!("/images/songs/{}.png", song.song_hash)},
None => {
// use a DefaultHasher to turn the song_hash string into a number we can
// use to give the song a psuedo-random default cover.
let mut hasher = DefaultHasher::new();
song.song_hash.hash(&mut hasher);
let hash = hasher.finish() as usize;
let cover_i = hash % model.default_song_covers.len();
let cover = model.default_song_covers[cover_i];
attrs! { At::Src => cover }
}
},
],
div![
C![C.song_item_info],
div![C![C.song_item_title], &song.title],
div![
C![C.song_item_artist],
span![&song.artist],
if let Some(year) = song.year.as_ref() {
span![" (", year, ")"]
} else {
empty![]
}
],
],
div![
C![C.song_gizmos],
match &song.genre {
Some(genre) => div![
C![C.gizmo, C.note_icon, C.tooltip],
span![C![C.tooltiptext], genre],
],
None => empty![],
},
match &song.language {
Some(language) => div![
C![C.gizmo, C.flag_icon, C.tooltip],
span![C![C.tooltiptext], language],
],
None => empty![],
},
IF![song.video.is_some() => div![
C![C.gizmo, C.video_icon, C.tooltip],
span![
C![C.tooltiptext],
"Musikvideo",
],
]],
match (&song.duet_singer_1, &song.duet_singer_2) {
(Some(p1), Some(p2)) => div![
C![C.gizmo, C.duet_icon, C.tooltip],
span![
C![C.tooltiptext],
"Duet",
div![
C![C.marquee],
// add duplicates to get the repeating marquee effect
p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2]
],
],
],
_ => empty![],
},
],
]
};
vec![
div![
C![C.song_search_bar],
input![
C![C.song_search_field],
input_ev(Ev::Input, Msg::Search),
attrs! {
At::Placeholder => &model.query_placeholder[..model.query_placeholder_len],
At::Value => model.query,
},
],
button![
C![C.song_sort_button, C.tooltip],
IF![model.filter_duets => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::ToggleDuets),
span![C![C.tooltiptext], "Endast Duetter"],
"D",
],
button![
C![C.song_sort_button, C.tooltip],
IF![model.filter_video => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::ToggleVideo),
span![C![C.tooltiptext], "Endast med Video"],
"V",
],
button![
C![C.song_sort_button, C.song_sort_button_right, C.tooltip],
IF![model.filter_video => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::Shuffle),
span![C![C.tooltiptext], "Blanda låtar"],
"🔀",
],
],
div![
C![C.song_list],
attrs! {At::Id => SONG_LIST_ID},
ev(Ev::Scroll, |_| Msg::Scroll),
model
.songs
.iter()
.map(|(_, song)| song)
.map(song_card)
.take(model.songs.len() - model.hidden_songs)
.take(model.shown_songs),
],
]
}
async fn fetch_songs() -> Option<Msg> {
let mut songs: Vec<Song> = match fetch_list_of("/songs").await {
Ok(response) => response,
Err(e) => {
error!("Error fetching songs:", e);
return None;
}
};
songs.shuffle(&mut thread_rng());
Some(Msg::Songs(songs))
}
pub fn autotype_song(model: &mut Model, orders: &mut impl Orders<Msg>) {
let (_, song) = &model.songs[0];
model.query_placeholder = ParsedQuery::random(song, &mut thread_rng()).to_string();
model.query_placeholder_len = 0;
model.autotyper = Some(orders.perform_cmd_with_handle(timeout(100, || Msg::Autotyper)));
}
const SONG_LIST_ID: &str = "song_list";
fn get_song_list_element() -> Option<Element> {
document().get_element_by_id(SONG_LIST_ID)
}
fn scroll_to_top() {
if let Some(elem) = get_song_list_element() {
elem.scroll_to_with_x_and_y(0.0, 0.0);
}
}
fn get_scroll() -> Option<(i32, i32)> {
let list = get_song_list_element()?;
let scroll = list.scroll_top();
let height = list.client_height();
let max = (list.scroll_height() - height).max(0);
Some((scroll, max))
}

View File

@ -4,4 +4,4 @@ use css_typegen::css_typegen;
// Generate rust types for css-classes.
// Used for autocompletion and extra compile-time checks.
css_typegen!("static/styles");
css_typegen!("frontend/static/styles");

View File

@ -0,0 +1,34 @@
use std::collections::{HashMap, HashSet};
use gloo_console::error;
use crate::{
app::{Loading, Msg},
fetch::fetch_list_of,
};
pub type CustomLists = HashMap<String, Loading<HashSet<String>>>;
pub async fn fetch_custom_song_list_index() -> Option<Msg> {
let custom_lists: Vec<String> = match fetch_list_of("/custom/lists").await {
Ok(response) => response,
Err(e) => {
error!("Failed fetching custom song list index:", e);
return None;
}
};
Some(Msg::CustomSongLists(custom_lists))
}
pub async fn fetch_custom_song_list(list: String) -> Option<Msg> {
let song_hashes: HashSet<String> = match fetch_list_of(format!("/custom/list/{list}")).await {
Ok(response) => response.into_iter().collect(),
Err(e) => {
error!("Failed fetching custom song list:", e);
return None;
}
};
Some(Msg::CustomSongList { list, song_hashes })
}

70
frontend/src/fetch.rs Normal file
View File

@ -0,0 +1,70 @@
use std::io::Cursor;
use gloo_net::http::{Request, Response};
use serde::de::DeserializeOwned;
use wasm_bindgen::{JsError, JsValue};
const HTTP_ACCEPT: &str = concat!("text/csv, ", "application/json;q=0.9");
#[derive(Debug, thiserror::Error)]
pub enum FetchError {
/// The request returned a non-2XX status code.
#[error("server responded with {code} {text}")]
Status { code: u16, text: String },
/// The response contained an unrecognized or missing content type.
#[error("unknown content type {0:?}")]
UnknownContentType(Option<String>),
/// Another error occured.
#[error("{0}")]
Other(#[from] gloo_net::Error),
#[error("error deserializing csv: {0}")]
Csv(#[from] csv::Error),
}
impl From<FetchError> for JsValue {
fn from(e: FetchError) -> Self {
JsError::new(&e.to_string()).into()
}
}
/// Perform a GET request.
pub async fn fetch(url: impl AsRef<str>) -> Result<Response, FetchError> {
let response = Request::get(url.as_ref())
.header("accept", HTTP_ACCEPT)
.send()
.await?;
if !response.ok() {
return Err(FetchError::Status {
code: response.status(),
text: response.status_text(),
});
}
Ok(response)
}
/// Perform a GET request and try to deserialize the response as a `Vec<T>`.
pub async fn fetch_list_of<T: DeserializeOwned>(
url: impl AsRef<str>,
) -> Result<Vec<T>, FetchError> {
let response = fetch(url.as_ref()).await?;
let headers = response.headers();
let content_type = headers.get("Content-Type").map(|s| s.to_lowercase());
match content_type.as_deref() {
Some("text/csv") => {
let text = response.text().await?;
let reader = csv::Reader::from_reader(Cursor::new(text)).into_deserialize();
let list = reader
.map(|r| r.map_err(FetchError::from))
.collect::<Result<_, _>>()?;
Ok(list)
}
Some("application/json") => Ok(response.json().await?),
_ => Err(FetchError::UnknownContentType(content_type)),
}
}

View File

@ -1,13 +1,13 @@
mod app;
mod css;
mod custom_list;
mod fetch;
mod fuzzy;
mod query;
mod song;
use seed::prelude::wasm_bindgen;
use seed::App;
#[wasm_bindgen(start)]
pub fn start() {
fn main() {
App::start("app", app::init, app::update, app::view);
}

View File

@ -1,3 +1,6 @@
use crate::song::Song;
use rand::seq::SliceRandom;
use rand::Rng;
use std::borrow::Cow;
use std::fmt::{self, Display, Formatter};
use std::ops::Not;
@ -8,10 +11,10 @@ pub struct ParsedQuery<'a> {
pub plain: Option<Cow<'a, str>>,
/// Query a specific title
pub title: Option<&'a str>,
pub title: Option<Cow<'a, str>>,
/// Query a specific artist
pub artist: Option<&'a str>,
pub artist: Option<Cow<'a, str>>,
/// Whether the song is a duet
pub duet: Option<bool>,
@ -27,6 +30,9 @@ pub struct ParsedQuery<'a> {
/// Query from a specifc year
pub year: Option<&'a str>,
/// Query songs from the specified custom list.
pub list: Option<&'a str>,
}
impl<'a> ParsedQuery<'a> {
@ -40,25 +46,84 @@ impl<'a> ParsedQuery<'a> {
for (k, v) in kvs {
match k {
"title" => parsed.title = Some(v),
"artist" => parsed.artist = Some(v),
"title" => parsed.title = Some(Cow::Borrowed(v)),
"artist" => parsed.artist = Some(Cow::Borrowed(v)),
"duet" => parsed.duet = parse_bool(v),
"video" => parsed.video = parse_bool(v),
"lang" => parsed.language = Some(v),
"genre" => parsed.genre = Some(v),
"year" => parsed.year = Some(v),
"list" => parsed.list = Some(v),
_ => {}
}
}
parsed
}
/// Generate a parsed query with a few random fields matching a song
pub fn random<R: Rng>(song: &'a Song, rng: &mut R) -> Self {
let until_space = |s: &'a str| -> &'a str { s.split_whitespace().next().unwrap_or("") };
let join_spaces = |s: &'a str| -> Cow<'a, str> {
let s = s.trim();
if s.contains(char::is_whitespace) {
s.replace(char::is_whitespace, "").into()
} else {
Cow::Borrowed(s)
}
};
let mut primary_fields: [&dyn Fn(Self) -> Self; 4] = [
&|query| Self {
plain: Some(Cow::Borrowed(&song.title)),
..query
},
&|query| Self {
plain: Some(Cow::Borrowed(&song.artist)),
..query
},
&|query| Self {
title: Some(join_spaces(&song.title)),
..query
},
&|query| Self {
artist: Some(join_spaces(&song.artist)),
..query
},
];
let mut extra_fields: [&dyn Fn(Self) -> Self; 3] = [
&|query| Self {
language: song.language.as_deref().map(until_space),
..query
},
&|query| Self {
genre: song.genre.as_deref().map(until_space),
..query
},
&|query| Self {
year: song.year.as_deref().map(until_space),
..query
},
];
primary_fields.shuffle(rng);
extra_fields.shuffle(rng);
let primary_fields = primary_fields.into_iter().take(1);
let extra_fields = extra_fields.into_iter().take(rng.gen_range(0..2));
primary_fields
.chain(extra_fields)
.fold(Self::default(), |query, field| field(query))
}
}
fn parse_bool(s: &str) -> Option<bool> {
match s {
"true" | "yes" => Some(true),
"false" | "no" => Some(false),
"true" | "yes" | "y" => Some(true),
"false" | "no" | "n" => Some(false),
_ => None,
}
}
@ -75,7 +140,7 @@ fn extract_plain(s: &str) -> Option<Cow<str>> {
a
});
plain.is_empty().not().then(|| Cow::Owned(plain))
plain.is_empty().not().then_some(Cow::Owned(plain))
}
fn extract_key_values(s: &str) -> impl Iterator<Item = (&str, &str)> {
@ -111,6 +176,7 @@ impl Display for ParsedQuery<'_> {
w("lang:", display(&self.language))?;
w("genre:", display(&self.genre))?;
w("year:", display(&self.year))?;
w("list:", display(&self.list))?;
Ok(())
}

View File

@ -1,14 +1,16 @@
use crate::app::Loading;
use crate::custom_list::CustomLists;
use crate::fuzzy::{self, FuzzyScore};
use crate::query::ParsedQuery;
use serde::Deserialize;
use std::cmp::max;
#[derive(Deserialize, Debug, Clone, Default)]
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Song {
pub title: String,
pub artist: String,
pub cover: Option<String>,
pub song_hash: String,
pub cover: Option<String>,
pub language: Option<String>,
pub video: Option<String>,
pub year: Option<String>,
@ -27,22 +29,21 @@ impl Song {
.zip(self.duet_singer_2.as_deref())
}
pub fn fuzzy_compare(&self, query: &ParsedQuery) -> FuzzyScore {
let bad = || -1;
pub fn fuzzy_compare(&self, query: &ParsedQuery, custom_lists: &CustomLists) -> FuzzyScore {
let bad: FuzzyScore = -1;
let filter_strs = |query: Option<&str>, item: Option<&str>| {
if let Some(query) = query {
match item {
Some(item) => {
let score = fuzzy::compare(item.chars(), query.chars());
if score < fuzzy::max_score(query) / 2 {
return false;
}
score == fuzzy::max_score(query)
}
None => return false,
None => false,
}
} else {
true
}
true
};
let filter_bool =
@ -57,7 +58,17 @@ impl Song {
];
if !filters.iter().all(|f| f()) {
return bad();
return bad;
}
if let Some(list) = query.list {
let Some(Loading::Loaded(list)) = custom_lists.get(list) else {
return bad;
};
if !list.contains(&self.song_hash) {
return bad;
}
}
let mut score = FuzzyScore::default();
@ -67,12 +78,12 @@ impl Song {
score = max(title_score, artist_score);
}
if let Some(title) = query.title {
if let Some(title) = &query.title {
let new_score = fuzzy::compare(self.title.chars(), title.chars());
score = max(score, new_score);
}
if let Some(artist) = query.artist {
if let Some(artist) = &query.artist {
let new_score = fuzzy::compare(self.artist.chars(), artist.chars());
score = max(score, new_score);
}

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="151.118px" height="151.118px" viewBox="0 0 151.118 151.118" enable-background="new 0 0 151.118 151.118" xml:space="preserve">
<circle fill="#FFFFFF" stroke="#000000" stroke-width="3" cx="75.559" cy="75.56" r="71.208"/>
<g>
<g>
<path fill="#00B3E6" stroke="#000000" d="M79.393,18.702c-1.27-0.085-2.543-0.143-3.833-0.143c-6.136,0-12.04,0.98-17.578,2.774 c0.458,1.249,0.721,2.592,0.721,4c0,6.429-5.211,11.64-11.64,11.64c-3.645,0-6.895-1.678-9.029-4.3 c-3.313,2.902-6.285,6.18-8.853,9.771h50.212V18.702z M103.104,25.654v16.79h18.835C117,35.541,110.568,29.783,103.104,25.654z M125.401,47.905h-22.298v46.557c0,0-1.151,11.496,11.495,11.496h9.169c5.562-8.801,8.792-19.22,8.792-30.397 C132.56,65.522,129.956,56.097,125.401,47.905z M79.393,94.461c0,0,0-23.276,0-34.917s-11.639-11.64-11.639-11.64h-7.759v46.557 c0,11.641,11.639,11.641,11.639,11.641v7.688H33.283c10.43,11.526,25.507,18.771,42.276,18.771 c16.77,0,31.846-7.244,42.275-18.771h-3.236C114.599,113.788,79.393,115.814,79.393,94.461z M36.717,59.545 c0-10.017-8.614-11.412-11.018-11.606c-4.544,8.184-7.14,17.597-7.14,27.621c0,11.17,3.226,21.58,8.777,30.376 c9.374-1.402,9.38-11.475,9.38-11.475S36.717,71.185,36.717,59.545z"/>
</g>
<g>
<path fill="#00B3E6" stroke="#000000" stroke-width="3" d="M79.393,18.702c-1.27-0.085-2.543-0.143-3.833-0.143 c-6.136,0-12.04,0.98-17.578,2.774c0.458,1.249,0.721,2.592,0.721,4c0,6.429-5.211,11.64-11.64,11.64 c-3.645,0-6.895-1.678-9.029-4.3c-3.313,2.902-6.285,6.18-8.853,9.771h50.212V18.702z M103.104,25.654v16.79h18.835 C117,35.541,110.568,29.783,103.104,25.654z M125.401,47.905h-22.298v46.557c0,0-1.151,11.496,11.495,11.496h9.169 c5.562-8.801,8.792-19.22,8.792-30.397C132.56,65.522,129.956,56.097,125.401,47.905z M79.393,94.461c0,0,0-23.276,0-34.917 s-11.639-11.64-11.639-11.64h-7.759v46.557c0,11.641,11.639,11.641,11.639,11.641v7.688H33.283 c10.43,11.526,25.507,18.771,42.276,18.771c16.77,0,31.846-7.244,42.275-18.771h-3.236 C114.599,113.788,79.393,115.814,79.393,94.461z M36.717,59.545c0-10.017-8.614-11.412-11.018-11.606 c-4.544,8.184-7.14,17.597-7.14,27.621c0,11.17,3.226,21.58,8.777,30.376c9.374-1.402,9.38-11.475,9.38-11.475 S36.717,71.185,36.717,59.545z"/>
</g>
</g>
<g>
<path d="M8.724,80.924l1.055-0.127l0.217,1.832l5.027-0.598l0.151,1.272l-5.027,0.597l0.217,1.831L9.31,85.857L8.724,80.924z"/>
<path d="M9.61,74.781l0.074,3.155l1.296-0.029l-0.067-2.891l1.062-0.024l0.067,2.889l1.578-0.036l-0.077-3.312l1.093-0.025 l0.107,4.562l-6.124,0.144l-0.103-4.405L9.61,74.781z"/>
<path d="M8.573,72.581l0.099-1.246l2.519,0.201L8.86,68.973l0.131-1.645l2.312,2.705l3.804-2.348l-0.13,1.637l-2.817,1.67 l0.576,0.666l2.042,0.163l-0.099,1.246L8.573,72.581z"/>
<path d="M9.257,65.512l0.247-1.311l4.612-1.558l-4.17-0.786l0.22-1.167l6.019,1.134l-0.235,1.251l-4.699,1.603l4.245,0.8 l-0.22,1.167L9.257,65.512z"/>
<path d="M17.4,55.628c0.32,0.581,0.351,1.289,0.095,2.124c-0.256,0.835-0.679,1.404-1.269,1.706 c-0.739,0.421-1.633,0.471-2.683,0.149c-1.071-0.328-1.783-0.87-2.139-1.626c-0.319-0.581-0.351-1.289-0.095-2.124 s0.679-1.404,1.269-1.706c0.717-0.425,1.611-0.474,2.682-0.146C16.31,54.327,17.023,54.868,17.4,55.628z M16.27,56.118 c-0.268-0.392-0.729-0.688-1.384-0.888c-0.652-0.2-1.2-0.213-1.643-0.039c-0.443,0.173-0.738,0.501-0.885,0.982 s-0.087,0.92,0.179,1.315c0.267,0.395,0.728,0.693,1.383,0.894c0.654,0.2,1.203,0.212,1.646,0.034 c0.442-0.178,0.738-0.507,0.885-0.989C16.598,56.946,16.538,56.51,16.27,56.118z"/>
<path d="M13.063,51.3l0.5-1.18l4.633,1.962l1.182-2.791l1.007,0.426l-1.682,3.971L13.063,51.3z"/>
<path d="M22.694,44.202c0.194,0.634,0.081,1.333-0.341,2.099c-0.421,0.765-0.951,1.236-1.59,1.411 c-0.81,0.262-1.695,0.128-2.657-0.401c-0.981-0.54-1.568-1.216-1.761-2.029c-0.194-0.633-0.081-1.333,0.34-2.098 c0.421-0.766,0.951-1.236,1.59-1.412c0.789-0.27,1.673-0.134,2.654,0.405C21.893,42.706,22.48,43.381,22.694,44.202z M21.487,44.451c-0.183-0.438-0.574-0.822-1.174-1.152c-0.598-0.329-1.131-0.453-1.6-0.374c-0.469,0.08-0.825,0.34-1.067,0.781 c-0.243,0.441-0.273,0.882-0.093,1.324c0.181,0.442,0.571,0.828,1.171,1.158s1.135,0.453,1.604,0.369 c0.469-0.084,0.826-0.346,1.068-0.787C21.64,45.329,21.67,44.889,21.487,44.451z"/>
<path d="M23.325,36.713c-0.397-0.163-0.773-0.14-1.126,0.071c-0.196,0.119-0.368,0.282-0.517,0.492 c-0.284,0.401-0.366,0.838-0.247,1.31c0.119,0.472,0.482,0.922,1.089,1.352c0.611,0.433,1.142,0.599,1.594,0.5 c0.452-0.099,0.803-0.328,1.056-0.684c0.247-0.349,0.35-0.706,0.308-1.07c-0.042-0.364-0.206-0.687-0.493-0.968l-0.824,1.164 l-0.829-0.587l1.472-2.079l2.678,1.896l-0.488,0.688l-0.692-0.347c0.094,0.369,0.132,0.667,0.116,0.896 c-0.027,0.395-0.182,0.793-0.466,1.195c-0.469,0.662-1.081,1.042-1.837,1.141c-0.771,0.115-1.556-0.11-2.353-0.675 c-0.807-0.571-1.296-1.251-1.468-2.04c-0.172-0.789,0-1.548,0.518-2.279c0.448-0.633,0.969-1.028,1.562-1.186 c0.593-0.158,1.15-0.094,1.669,0.189L23.325,36.713z"/>
<path d="M27.742,34.873c0.234,0.155,0.441,0.236,0.62,0.241c0.326,0.01,0.656-0.175,0.992-0.554 c0.201-0.227,0.339-0.434,0.415-0.619c0.143-0.353,0.085-0.643-0.172-0.871c-0.151-0.133-0.326-0.17-0.525-0.111 c-0.197,0.061-0.453,0.208-0.768,0.439l-0.541,0.394c-0.532,0.386-0.943,0.616-1.233,0.69c-0.489,0.128-0.957-0.006-1.406-0.402 c-0.409-0.361-0.616-0.813-0.62-1.354s0.251-1.103,0.768-1.688c0.431-0.488,0.927-0.791,1.487-0.909 c0.561-0.118,1.092,0.029,1.594,0.443l-0.817,0.925c-0.291-0.227-0.594-0.277-0.911-0.152c-0.211,0.084-0.416,0.238-0.615,0.463 c-0.221,0.251-0.348,0.495-0.381,0.732c-0.033,0.237,0.04,0.435,0.218,0.591c0.164,0.145,0.351,0.179,0.562,0.103 c0.137-0.047,0.378-0.193,0.723-0.438l0.894-0.632c0.392-0.277,0.734-0.442,1.028-0.496c0.456-0.083,0.888,0.056,1.295,0.416 c0.418,0.369,0.623,0.837,0.614,1.403c-0.009,0.566-0.273,1.143-0.792,1.731c-0.531,0.601-1.085,0.953-1.662,1.056 c-0.577,0.103-1.104-0.056-1.581-0.478L27.742,34.873z"/>
<path d="M32.433,25.651l-2.314,2.146L31,28.748l2.12-1.965l0.722,0.779l-2.12,1.965l1.073,1.157l2.429-2.252l0.744,0.802 l-3.346,3.102l-4.164-4.492l3.231-2.996L32.433,25.651z"/>
<path d="M33.397,23.413l1-0.75l1.518,2.021l0.377-3.443l1.319-0.991l-0.497,3.523l4.278,1.297l-1.313,0.986l-3.117-1.006 l-0.118,0.873l1.23,1.638l-1,0.75L33.397,23.413z"/>
<path d="M42.951,16.963l0.55,0.909l-1.577,0.956l2.623,4.33l-1.096,0.664l-2.623-4.33l-1.577,0.956L38.7,19.539L42.951,16.963z"/>
<path d="M48.265,21.134l-1.14,0.585l-2.797-5.449l1.14-0.585L48.265,21.134z"/>
<path d="M53.806,18.069c-0.244,0.616-0.769,1.092-1.575,1.428c-0.807,0.335-1.515,0.373-2.123,0.111 c-0.796-0.3-1.405-0.958-1.826-1.971c-0.43-1.034-0.467-1.928-0.111-2.685c0.244-0.616,0.769-1.092,1.575-1.428 s1.514-0.373,2.123-0.111c0.785,0.282,1.392,0.939,1.822,1.973C54.113,16.399,54.151,17.294,53.806,18.069z M52.707,17.511 c0.13-0.456,0.064-1-0.199-1.633c-0.262-0.629-0.601-1.06-1.017-1.29c-0.417-0.23-0.857-0.249-1.322-0.055 c-0.465,0.194-0.764,0.519-0.898,0.977s-0.069,1.003,0.193,1.635c0.263,0.632,0.604,1.062,1.024,1.29 c0.42,0.228,0.861,0.245,1.326,0.051C52.279,18.292,52.577,17.967,52.707,17.511z"/>
<path d="M54.397,11.942l1.28-0.375l3.48,3.403l-1.191-4.073l1.14-0.333l1.72,5.878L59.604,16.8l-3.561-3.459l1.213,4.146 l-1.14,0.333L54.397,11.942z"/>
<path d="M65.833,10.335l-3.104,0.573l0.235,1.275l2.843-0.525L66,12.703l-2.843,0.524l0.287,1.552l3.257-0.602l0.199,1.076 l-4.487,0.828l-1.112-6.023l4.333-0.8L65.833,10.335z"/>
<path d="M67.808,8.965l1.33-0.107l2.718,4.039l-0.341-4.23l1.184-0.095l0.492,6.105l-1.269,0.102l-2.785-4.109l0.347,4.306 L68.3,15.07L67.808,8.965z"/>
<path d="M78.854,14.759l-1.279-0.055l0.262-6.119l1.279,0.055L78.854,14.759z"/>
<path d="M81.168,8.739l1.324,0.159l1.862,4.497l0.505-4.213l1.18,0.141l-0.729,6.082l-1.265-0.151l-1.914-4.581l-0.514,4.289 l-1.18-0.141L81.168,8.739z"/>
<path d="M88.194,9.732l4.24,0.942l-0.23,1.037l-2.988-0.665l-0.31,1.388l2.623,0.583l-0.229,1.037l-2.623-0.583l-0.557,2.501 l-1.252-0.278L88.194,9.732z"/>
<path d="M96.917,17.911c-0.588,0.305-1.297,0.318-2.125,0.042c-0.828-0.276-1.388-0.713-1.674-1.311 c-0.402-0.75-0.431-1.645-0.082-2.686c0.354-1.062,0.914-1.761,1.678-2.097c0.59-0.305,1.298-0.319,2.127-0.042 c0.828,0.276,1.387,0.713,1.674,1.311c0.406,0.728,0.434,1.623,0.079,2.685C98.245,16.854,97.687,17.553,96.917,17.911z M96.456,16.769c0.396-0.258,0.705-0.712,0.922-1.362c0.215-0.647,0.242-1.194,0.08-1.641c-0.162-0.447-0.482-0.75-0.961-0.91 c-0.478-0.159-0.916-0.11-1.318,0.147c-0.401,0.257-0.711,0.71-0.928,1.36c-0.217,0.65-0.242,1.198-0.076,1.645 c0.168,0.447,0.49,0.75,0.968,0.91S96.057,17.027,96.456,16.769z"/>
<path d="M104.888,15.472c0.164,0.182,0.276,0.392,0.341,0.628c0.053,0.195,0.074,0.394,0.064,0.596 c-0.012,0.202-0.066,0.413-0.168,0.631c-0.121,0.264-0.309,0.492-0.559,0.685c-0.251,0.193-0.554,0.272-0.908,0.236 c0.207,0.222,0.32,0.452,0.34,0.691c0.019,0.239-0.062,0.554-0.242,0.945l-0.174,0.374c-0.117,0.254-0.188,0.432-0.209,0.532 c-0.037,0.158-0.016,0.296,0.063,0.415l-0.063,0.14l-1.274-0.588c0.022-0.14,0.044-0.25,0.063-0.333 c0.043-0.17,0.101-0.337,0.174-0.502l0.23-0.521c0.159-0.357,0.208-0.623,0.145-0.796c-0.062-0.173-0.258-0.334-0.582-0.484 l-1.137-0.526l-1.01,2.185l-1.135-0.524l2.568-5.56l2.658,1.229C104.454,15.106,104.724,15.289,104.888,15.472z M102.106,15.184 l-0.688,1.489l1.252,0.578c0.248,0.115,0.449,0.171,0.602,0.168c0.27-0.003,0.475-0.161,0.618-0.472 c0.155-0.336,0.153-0.611-0.007-0.825c-0.09-0.121-0.254-0.236-0.492-0.346L102.106,15.184z"/>
<path d="M111.327,18.894l1.566,0.953l-3.18,5.235l-1.014-0.616l2.149-3.541c0.062-0.102,0.149-0.244,0.263-0.426 c0.114-0.182,0.201-0.323,0.262-0.422l-3.656,3.792l-1.062-0.644l1.687-4.99c-0.062,0.1-0.146,0.242-0.256,0.427 c-0.107,0.185-0.193,0.328-0.256,0.43l-2.149,3.541l-1.015-0.616l3.179-5.235l1.586,0.963l-1.55,4.693L111.327,18.894z"/>
<path d="M116.072,22.188l1.153,0.892l-2.041,6.165l-1.096-0.847l0.449-1.233l-1.783-1.379l-1.09,0.737l-1.065-0.825L116.072,22.188 z M113.683,25.16l1.237,0.957l0.865-2.379L113.683,25.16z"/>
<path d="M123.066,28.23l-0.724,0.778l-1.352-1.255l-3.445,3.708l-0.938-0.872l3.446-3.708l-1.352-1.255l0.725-0.778L123.066,28.23z "/>
<path d="M120.504,34.474l-0.875-0.936l4.475-4.182l0.875,0.936L120.504,34.474z"/>
<path d="M125.021,39.041c-0.66-0.067-1.261-0.443-1.804-1.128c-0.541-0.685-0.77-1.356-0.684-2.013 c0.073-0.848,0.541-1.612,1.401-2.293c0.878-0.695,1.729-0.973,2.553-0.836c0.659,0.067,1.261,0.443,1.804,1.128 c0.541,0.685,0.77,1.356,0.684,2.013c-0.059,0.832-0.525,1.595-1.403,2.29C126.709,38.882,125.86,39.162,125.021,39.041z M125.258,37.832c0.476,0.001,0.98-0.21,1.519-0.635c0.534-0.423,0.856-0.866,0.966-1.329c0.107-0.463,0.006-0.892-0.307-1.287 s-0.707-0.595-1.186-0.6c-0.477-0.005-0.982,0.206-1.521,0.63c-0.538,0.424-0.858,0.87-0.963,1.335 c-0.104,0.465-0.002,0.896,0.312,1.29C124.391,37.632,124.784,37.83,125.258,37.832z"/>
<path d="M131.081,37.952l0.705,1.131l-2.334,4.271l3.6-2.249l0.629,1.007l-5.194,3.246l-0.674-1.079l2.367-4.364l-3.664,2.29 l-0.629-1.007L131.081,37.952z"/>
<path d="M131.352,46.758c-0.232,0.158-0.387,0.318-0.46,0.481c-0.134,0.297-0.089,0.673,0.135,1.128 c0.134,0.272,0.271,0.479,0.414,0.62c0.271,0.266,0.562,0.324,0.87,0.172c0.182-0.088,0.281-0.236,0.303-0.443 c0.02-0.206-0.019-0.498-0.113-0.877l-0.156-0.65c-0.154-0.639-0.211-1.106-0.168-1.403c0.066-0.5,0.369-0.882,0.908-1.146 c0.49-0.24,0.985-0.259,1.486-0.057c0.502,0.202,0.924,0.653,1.268,1.354c0.287,0.584,0.377,1.158,0.272,1.722 c-0.104,0.563-0.442,0.998-1.019,1.304l-0.543-1.108c0.32-0.182,0.484-0.443,0.49-0.784c0.002-0.227-0.062-0.475-0.194-0.745 c-0.147-0.3-0.324-0.511-0.531-0.631c-0.207-0.121-0.417-0.129-0.63-0.025c-0.195,0.096-0.299,0.256-0.309,0.48 c-0.01,0.146,0.033,0.424,0.129,0.835l0.242,1.067c0.106,0.468,0.129,0.848,0.065,1.14c-0.099,0.453-0.39,0.8-0.878,1.039 c-0.501,0.245-1.012,0.256-1.531,0.032c-0.521-0.224-0.953-0.688-1.298-1.393c-0.353-0.72-0.467-1.366-0.342-1.938 c0.124-0.573,0.473-0.999,1.044-1.279L131.352,46.758z"/>
<path d="M139.122,54.22l-0.994,0.375l-0.651-1.725l-4.736,1.787l-0.452-1.199l4.736-1.787l-0.65-1.725l0.994-0.375L139.122,54.22z"/>
<path d="M139.737,60.395l-0.83-3.045l-1.251,0.341l0.761,2.789l-1.025,0.279l-0.76-2.789l-1.522,0.415l0.871,3.196l-1.056,0.288 l-1.199-4.402l5.908-1.611l1.158,4.251L139.737,60.395z"/>
<path d="M141.273,62.279l0.204,1.233l-2.493,0.413l2.881,1.925l0.271,1.627l-2.896-2.068l-3.126,3.195l-0.268-1.62l2.331-2.3 l-0.721-0.507l-2.021,0.334l-0.204-1.233L141.273,62.279z"/>
<path d="M142.313,69.3l0.077,1.331l-4.099,2.627l4.236-0.246l0.069,1.185l-6.114,0.356l-0.074-1.271l4.172-2.693l-4.313,0.251 l-0.067-1.186L142.313,69.3z"/>
<path d="M136.42,77.345l0.022-1.281l6.124,0.11l-0.023,1.281L136.42,77.345z"/>
<path d="M142.495,79.542l-0.119,1.244l-2.516-0.238l2.293,2.598l-0.156,1.643l-2.271-2.738l-3.839,2.289l0.154-1.635l2.842-1.627 l-0.565-0.675l-2.04-0.192l0.118-1.244L142.495,79.542z"/>
</g>
<rect x="13.962" y="14.947" fill="none" width="121.094" height="121.095"/>
<g>
<path d="M14.433,92.2c-0.122,0.209-0.31,0.348-0.566,0.416c-0.256,0.065-0.49,0.038-0.702-0.084 c-0.212-0.123-0.351-0.312-0.418-0.568s-0.039-0.488,0.085-0.699s0.315-0.35,0.571-0.417c0.256-0.067,0.488-0.039,0.697,0.084 c0.209,0.124,0.348,0.313,0.415,0.569C14.583,91.756,14.555,91.989,14.433,92.2z"/>
<path d="M17.734,96.76c0.649,0.271,1.115,0.759,1.398,1.464c0.377,0.942,0.34,1.76-0.112,2.446 c-0.256,0.383-0.558,0.654-0.905,0.812l-0.476-1.189c0.242-0.184,0.409-0.361,0.5-0.535c0.163-0.309,0.161-0.67-0.004-1.082 c-0.168-0.42-0.475-0.684-0.92-0.786c-0.445-0.103-0.985-0.027-1.621,0.228c-0.636,0.254-1.062,0.572-1.276,0.954 c-0.215,0.382-0.244,0.771-0.086,1.162c0.162,0.403,0.421,0.655,0.78,0.759c0.194,0.057,0.454,0.065,0.779,0.024l0.476,1.188 c-0.659,0.146-1.265,0.088-1.819-0.174c-0.554-0.263-0.962-0.72-1.223-1.371c-0.323-0.807-0.315-1.545,0.023-2.216 c0.341-0.672,0.969-1.19,1.884-1.558C16.121,96.493,16.988,96.45,17.734,96.76z"/>
<path d="M15.157,104.76l5.418-2.856l0.597,1.134l-2.059,1.086l1.093,2.073l2.06-1.086l0.598,1.134l-5.418,2.855l-0.598-1.133 l2.419-1.275l-1.093-2.072l-2.418,1.274L15.157,104.76z"/>
<path d="M24.609,108.986l0.812,1.211l-3.888,5.201l-0.771-1.149l0.816-1.027l-1.254-1.872l-1.268,0.354l-0.75-1.121L24.609,108.986 z M21.401,111.047l0.871,1.301l1.575-1.982L21.401,111.047z"/>
<path d="M27.297,112.821l0.813,0.989l-3.886,3.194l1.925,2.343l-0.845,0.694l-2.739-3.332L27.297,112.821z"/>
<path d="M33.701,119.881l1.308,1.287l-4.295,4.365l-0.847-0.832l2.905-2.953c0.083-0.086,0.201-0.203,0.354-0.354 c0.152-0.151,0.27-0.269,0.351-0.353l-4.43,2.854l-0.885-0.87l2.786-4.471c-0.082,0.083-0.197,0.202-0.346,0.356 c-0.149,0.155-0.265,0.274-0.348,0.359l-2.905,2.953l-0.846-0.832l4.295-4.366l1.323,1.302l-2.584,4.213L33.701,119.881z"/>
<path d="M38.937,125.708l-2.477-1.957l-0.804,1.019l2.269,1.792l-0.659,0.834l-2.268-1.792l-0.979,1.237l2.599,2.054l-0.678,0.858 l-3.58-2.828l3.797-4.807l3.457,2.731L38.937,125.708z"/>
<path d="M44.062,127.978c0.138,0.203,0.222,0.426,0.253,0.669c0.026,0.2,0.02,0.4-0.018,0.6c-0.038,0.198-0.121,0.399-0.251,0.604 c-0.156,0.245-0.372,0.445-0.646,0.604c-0.275,0.157-0.585,0.194-0.932,0.11c0.176,0.247,0.257,0.491,0.243,0.729 c-0.014,0.24-0.137,0.541-0.369,0.903l-0.222,0.349c-0.151,0.234-0.244,0.401-0.279,0.498c-0.057,0.15-0.055,0.291,0.007,0.42 l-0.083,0.129l-1.183-0.756c0.041-0.135,0.077-0.241,0.108-0.32c0.065-0.163,0.145-0.321,0.24-0.475l0.3-0.484 c0.206-0.333,0.29-0.59,0.25-0.77c-0.04-0.181-0.209-0.367-0.511-0.56l-1.056-0.675l-1.296,2.027l-1.053-0.674l3.298-5.16 l2.468,1.578C43.681,127.557,43.924,127.774,44.062,127.978z M41.345,127.314l-0.883,1.383l1.162,0.743 c0.23,0.146,0.421,0.23,0.573,0.248c0.267,0.032,0.492-0.096,0.677-0.384c0.199-0.312,0.234-0.584,0.105-0.817 c-0.073-0.132-0.22-0.27-0.441-0.41L41.345,127.314z"/>
<path d="M45.217,133.312c-0.091,0.267-0.117,0.485-0.078,0.66c0.073,0.317,0.334,0.591,0.786,0.819 c0.271,0.138,0.505,0.22,0.704,0.246c0.377,0.049,0.644-0.08,0.799-0.387c0.091-0.18,0.083-0.357-0.025-0.535 c-0.109-0.177-0.315-0.387-0.619-0.634l-0.517-0.424c-0.507-0.417-0.834-0.757-0.979-1.021c-0.247-0.439-0.235-0.928,0.035-1.461 c0.247-0.488,0.632-0.802,1.154-0.941c0.522-0.141,1.131-0.034,1.826,0.318c0.581,0.295,0.999,0.697,1.254,1.211 c0.255,0.513,0.246,1.063-0.028,1.652l-1.101-0.559c0.146-0.338,0.119-0.646-0.082-0.92c-0.134-0.184-0.335-0.342-0.604-0.479 c-0.298-0.15-0.566-0.213-0.804-0.186s-0.411,0.147-0.518,0.359c-0.099,0.194-0.084,0.385,0.042,0.568 c0.081,0.121,0.282,0.316,0.605,0.59l0.836,0.706c0.367,0.31,0.613,0.601,0.739,0.87c0.195,0.421,0.169,0.873-0.077,1.357 c-0.252,0.498-0.653,0.812-1.204,0.947c-0.55,0.135-1.175,0.023-1.875-0.332c-0.715-0.361-1.195-0.81-1.44-1.342 c-0.245-0.531-0.223-1.082,0.064-1.65L45.217,133.312z"/>
<path d="M58.45,134.226l-0.344,1.005l-1.744-0.598l-1.642,4.787l-1.212-0.414l1.642-4.789l-1.744-0.598l0.345-1.006L58.45,134.226z "/>
<path d="M63.51,136.602l-3.069-0.738l-0.303,1.262l2.811,0.676l-0.249,1.033l-2.811-0.676l-0.369,1.533l3.221,0.773l-0.256,1.063 l-4.436-1.067l1.433-5.953l4.284,1.029L63.51,136.602z"/>
<path d="M65.268,135.827l1.238,0.168l-0.338,2.504l2.687-2.188l1.635,0.223l-2.828,2.158l2.135,3.928l-1.627-0.221l-1.513-2.904 l-0.697,0.539l-0.274,2.029l-1.239-0.168L65.268,135.827z"/>
<path d="M71.6,136.6l1.333,0.039l2.259,4.312l0.125-4.242l1.188,0.035l-0.18,6.123l-1.272-0.037l-2.317-4.392l-0.127,4.317 l-1.187-0.034L71.6,136.6z"/>
<path d="M79.522,142.725l-1.279,0.059l-0.277-6.119l1.279-0.059L79.522,142.725z"/>
<path d="M82.211,140.633c0.069,0.271,0.17,0.47,0.3,0.593c0.235,0.226,0.604,0.31,1.108,0.249c0.301-0.035,0.541-0.096,0.723-0.184 c0.341-0.168,0.492-0.422,0.451-0.765c-0.022-0.2-0.129-0.344-0.315-0.433c-0.188-0.088-0.478-0.148-0.865-0.186l-0.666-0.068 c-0.653-0.066-1.112-0.17-1.379-0.31c-0.448-0.229-0.709-0.643-0.778-1.237c-0.062-0.543,0.084-1.017,0.441-1.422 c0.357-0.406,0.924-0.654,1.698-0.744c0.646-0.076,1.218,0.029,1.714,0.315s0.793,0.751,0.891,1.394l-1.226,0.145 c-0.065-0.363-0.257-0.604-0.576-0.723c-0.213-0.078-0.469-0.1-0.769-0.064c-0.332,0.039-0.59,0.137-0.771,0.29 c-0.184,0.155-0.261,0.351-0.232,0.586c0.024,0.218,0.143,0.367,0.351,0.451c0.134,0.058,0.41,0.108,0.83,0.155l1.087,0.127 c0.477,0.055,0.843,0.16,1.098,0.316c0.396,0.243,0.623,0.635,0.688,1.174c0.064,0.555-0.096,1.039-0.479,1.455 c-0.386,0.416-0.968,0.67-1.746,0.76c-0.797,0.094-1.443-0.014-1.941-0.322s-0.784-0.777-0.858-1.41L82.211,140.633z"/>
<path d="M86.725,135.772l1.221-0.27l0.543,2.469l1.771-2.979l1.611-0.354l-1.914,3l3.355,2.953l-1.604,0.354l-2.42-2.207 l-0.47,0.745l0.44,2.001l-1.221,0.27L86.725,135.772z"/>
<path d="M94.597,133.771l1.384-0.459l3.979,5.133l-1.313,0.437l-0.772-1.061l-2.14,0.71l0.004,1.316l-1.28,0.425L94.597,133.771z M95.729,137.413l1.484-0.494l-1.491-2.047L95.729,137.413z"/>
<path d="M104.025,136.72l-2.748-5.476l1.146-0.574l1.045,2.082l2.095-1.053l-1.044-2.08l1.145-0.574l2.748,5.474l-1.146,0.575 l-1.228-2.444l-2.095,1.052l1.228,2.443L104.025,136.72z"/>
<path d="M107.144,126.129l0.918-0.594l0.535,0.827l-0.919,0.595L107.144,126.129z M113.952,130.424 c-0.129,0.65-0.562,1.212-1.295,1.687c-0.732,0.474-1.423,0.638-2.068,0.488c-0.836-0.154-1.553-0.69-2.148-1.613 c-0.607-0.94-0.804-1.813-0.588-2.622c0.129-0.648,0.561-1.212,1.294-1.686c0.733-0.475,1.424-0.637,2.069-0.488 c0.822,0.137,1.537,0.676,2.146,1.615C113.956,128.727,114.153,129.6,113.952,130.424z M108.547,125.223l0.918-0.594l0.535,0.826 l-0.918,0.594L108.547,125.223z M112.771,130.071c0.047-0.473-0.115-0.996-0.487-1.57c-0.369-0.572-0.779-0.937-1.229-1.088 c-0.451-0.152-0.888-0.093-1.312,0.182c-0.423,0.273-0.658,0.646-0.709,1.121c-0.051,0.476,0.11,1,0.481,1.574 c0.372,0.576,0.784,0.938,1.237,1.086s0.892,0.088,1.313-0.187S112.725,130.543,112.771,130.071z"/>
<path d="M117.1,123.377c-0.342-0.262-0.709-0.338-1.104-0.229c-0.222,0.062-0.432,0.176-0.629,0.337 c-0.381,0.312-0.576,0.711-0.586,1.198c-0.01,0.486,0.221,1.018,0.692,1.592c0.476,0.579,0.942,0.881,1.403,0.903 c0.463,0.022,0.861-0.103,1.199-0.38c0.332-0.271,0.524-0.588,0.582-0.95c0.055-0.362-0.018-0.718-0.22-1.063l-1.103,0.903 l-0.646-0.785l1.97-1.614l2.08,2.537l-0.652,0.535l-0.575-0.518c-0.007,0.38-0.05,0.678-0.125,0.894 c-0.132,0.374-0.386,0.718-0.767,1.029c-0.627,0.515-1.318,0.719-2.074,0.612c-0.773-0.094-1.471-0.518-2.09-1.272 c-0.627-0.765-0.92-1.55-0.877-2.354c0.043-0.806,0.412-1.494,1.104-2.062c0.602-0.491,1.208-0.735,1.82-0.729 c0.615,0.006,1.135,0.215,1.561,0.625L117.1,123.377z"/>
<path d="M121.618,122.366c0.225,0.168,0.426,0.26,0.604,0.274c0.324,0.028,0.665-0.14,1.021-0.499 c0.213-0.217,0.361-0.414,0.447-0.596c0.162-0.344,0.12-0.637-0.124-0.879c-0.144-0.142-0.315-0.188-0.519-0.14 c-0.2,0.05-0.465,0.183-0.791,0.396l-0.562,0.363c-0.553,0.356-0.976,0.564-1.271,0.623c-0.494,0.102-0.954-0.06-1.381-0.479 c-0.39-0.384-0.57-0.847-0.545-1.386c0.024-0.541,0.312-1.088,0.858-1.644c0.457-0.464,0.97-0.739,1.534-0.825 c0.566-0.089,1.089,0.088,1.566,0.528l-0.867,0.879c-0.276-0.241-0.577-0.31-0.9-0.202c-0.215,0.072-0.428,0.215-0.641,0.43 c-0.233,0.238-0.375,0.476-0.42,0.709c-0.047,0.235,0.016,0.438,0.185,0.604c0.156,0.152,0.341,0.198,0.556,0.133 c0.141-0.039,0.389-0.172,0.746-0.396l0.928-0.582c0.406-0.254,0.758-0.4,1.054-0.438c0.46-0.058,0.884,0.104,1.271,0.486 c0.397,0.393,0.575,0.869,0.536,1.436c-0.04,0.563-0.336,1.125-0.887,1.685c-0.562,0.571-1.136,0.892-1.718,0.963 c-0.58,0.071-1.098-0.116-1.551-0.563L121.618,122.366z"/>
<path d="M122.098,115.749l0.783-0.975l1.97,1.584l-0.483-3.432l1.033-1.284l0.389,3.537l4.465,0.2l-1.027,1.279l-3.27-0.205 l0.102,0.875l1.597,1.283l-0.782,0.975L122.098,115.749z"/>
<path d="M133.273,109.766c0.151,0.646-0.009,1.336-0.479,2.07c-0.472,0.735-1.031,1.17-1.682,1.302 c-0.824,0.207-1.699,0.014-2.624-0.578c-0.942-0.604-1.483-1.317-1.62-2.144c-0.152-0.646,0.008-1.336,0.479-2.07 c0.471-0.735,1.03-1.17,1.681-1.301c0.807-0.217,1.68-0.022,2.622,0.582C132.575,108.219,133.116,108.932,133.273,109.766z M132.053,109.934c-0.152-0.449-0.518-0.857-1.094-1.229c-0.574-0.369-1.099-0.529-1.571-0.48 c-0.474,0.049-0.847,0.283-1.117,0.708c-0.271,0.424-0.332,0.862-0.183,1.313c0.15,0.453,0.516,0.864,1.092,1.233 c0.576,0.37,1.102,0.528,1.576,0.478c0.474-0.051,0.847-0.291,1.119-0.715C132.146,110.819,132.206,110.383,132.053,109.934z"/>
<path d="M129.695,104.815l0.579-1.143l4.486,2.277l1.371-2.703l0.977,0.494l-1.952,3.846L129.695,104.815z"/>
<path d="M132.765,98.278l0.53-1.357l6.49,0.221l-0.505,1.289l-1.31-0.083l-0.82,2.1l1.016,0.839l-0.492,1.256L132.765,98.278z M136.29,99.733l0.568-1.457l-2.527-0.16L136.29,99.733z"/>
<path d="M137.782,90.629c0.208-0.125,0.44-0.153,0.697-0.088c0.256,0.065,0.447,0.205,0.572,0.414 c0.126,0.211,0.154,0.443,0.089,0.699c-0.065,0.258-0.204,0.446-0.415,0.57c-0.212,0.124-0.445,0.152-0.702,0.087 c-0.256-0.065-0.444-0.204-0.567-0.413c-0.123-0.21-0.15-0.443-0.086-0.699C137.437,90.944,137.575,90.754,137.782,90.629z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -18,6 +18,17 @@ body {
font-family: Ubuntu,serif;
}
body::after {
display: absolute;
top: 0;
bottom: 100vh;
left: 0;
right: 100vw;
background-image: url(/images/background.svg);
background-repeat: no-repeat;
}
.nobr {
white-space: nowrap;
}
@ -38,7 +49,7 @@ body {
.song_search_bar {
position: relative;
max-width: 38em;
width: 80%;
width: 95%;
margin: auto;
margin-top: 1em;
}

View File

@ -1,252 +0,0 @@
use crate::css::C;
use crate::query::ParsedQuery;
use crate::song::Song;
use anyhow::anyhow;
use rand::seq::SliceRandom;
use rand::thread_rng;
use seed::app::cmds::timeout;
use seed::browser::util::document;
use seed::prelude::*;
use seed::{attrs, button, div, empty, error, img, input, p, span, C, IF};
use std::cmp::Reverse;
use web_sys::Element;
pub struct Model {
songs: Vec<Song>,
query: String,
filtered_songs: Vec<usize>,
hidden_songs: usize,
shown_songs: usize,
filter_video: bool,
filter_duets: bool,
}
const SCROLL_THRESHOLD: usize = 50;
const INITIAL_ELEM_COUNT: usize = 100;
pub enum Msg {
Search(String),
ToggleVideo,
ToggleDuets,
Shuffle,
Scroll,
}
pub fn init(_url: Url, _orders: &mut impl Orders<Msg>) -> Model {
let songs: Vec<Song> =
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
let mut filtered_songs: Vec<usize> = (0..songs.len()).collect();
filtered_songs.shuffle(&mut thread_rng());
Model {
songs,
query: String::new(),
filtered_songs,
hidden_songs: 0,
shown_songs: INITIAL_ELEM_COUNT,
filter_video: false,
filter_duets: false,
}
}
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Search(query) => {
model.hidden_songs = 0;
model.shown_songs = INITIAL_ELEM_COUNT;
scroll_to_top();
model.query = query;
if model.query.is_empty() {
model.filter_duets = false;
model.filter_video = false;
update(Msg::Shuffle, model, orders)
} else {
let query = ParsedQuery::parse(&model.query);
model.filtered_songs.sort_by_cached_key(|&i| {
let song = &model.songs[i];
let score = song.fuzzy_compare(&query);
if score < Default::default() {
model.hidden_songs += 1;
}
let top_score = Reverse(score);
(top_score, &song.title, &song.artist, &song.song_hash)
});
model.filter_duets = query.duet == Some(true);
model.filter_video = query.video == Some(true);
}
}
Msg::ToggleVideo => {
let mut query = ParsedQuery::parse(&model.query);
query.video = match query.video {
Some(true) => None,
None | Some(false) => Some(true),
};
update(Msg::Search(query.to_string()), model, orders);
}
Msg::ToggleDuets => {
let mut query = ParsedQuery::parse(&model.query);
query.duet = match query.duet {
Some(true) => None,
None | Some(false) => Some(true),
};
update(Msg::Search(query.to_string()), model, orders);
}
Msg::Shuffle => {
model.hidden_songs = 0;
model.shown_songs = INITIAL_ELEM_COUNT;
scroll_to_top();
model.query.clear();
model.filtered_songs.shuffle(&mut thread_rng());
}
Msg::Scroll => {
let (scroll, max_scroll) = match get_scroll() {
Ok(v) => v,
Err(e) => {
error!(e);
return;
}
};
let scroll_left: i32 = max_scroll - scroll;
// when there are fewer elements than this below the scroll viewport, add more
const ELEMENT_HEIGHT: i32 = 48;
if scroll_left < ELEMENT_HEIGHT * SCROLL_THRESHOLD as i32 {
model.shown_songs += 1;
orders.perform_cmd(timeout(32 /* ms */, || Msg::Scroll));
}
}
}
}
pub fn view(model: &Model) -> Vec<Node<Msg>> {
let song_card = |song: &Song| -> Node<Msg> {
div![
C![C.song_item],
img![
C![C.song_item_cover],
match song.cover {
Some(_) => attrs! {At::Src => format!("/images/songs/{}.png", song.song_hash)},
None => attrs! {At::Src => "/images/default_cover.png"},
},
],
div![
C![C.song_item_info],
div![C![C.song_item_title], song.title.to_string()],
div![C![C.song_item_artist], song.artist.to_string()],
],
div![
C![C.song_gizmos],
match &song.genre {
Some(genre) => div![
C![C.gizmo, C.note_icon, C.tooltip],
span![C![C.tooltiptext], genre],
],
None => empty![],
},
match &song.language {
Some(language) => div![
C![C.gizmo, C.flag_icon, C.tooltip],
span![C![C.tooltiptext], language],
],
None => empty![],
},
IF![song.video.is_some() => div![
C![C.gizmo, C.video_icon, C.tooltip],
span![
C![C.tooltiptext],
"Musikvideo",
],
]],
match (&song.duet_singer_1, &song.duet_singer_2) {
(Some(p1), Some(p2)) => div![
C![C.gizmo, C.duet_icon, C.tooltip],
span![
C![C.tooltiptext],
"Duet",
div![
C![C.marquee],
p![" 🗲 ", p1, " 🗲 ", p2, " 🗲 ", p1, " 🗲 ", p2]
],
],
],
_ => empty![],
},
],
]
};
vec![
div![
C![C.song_search_bar],
input![
input_ev(Ev::Input, Msg::Search),
attrs! {
At::Placeholder => "Sök",
At::Value => model.query,
},
C![C.song_search_field],
],
button![
C![C.song_sort_button, C.tooltip],
IF![model.filter_duets => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::ToggleDuets),
span![C![C.tooltiptext], "Endast Duetter"],
"D",
],
button![
C![C.song_sort_button, C.tooltip],
IF![model.filter_video => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::ToggleVideo),
span![C![C.tooltiptext], "Endast med Video"],
"V",
],
button![
C![C.song_sort_button, C.song_sort_button_right, C.tooltip],
IF![model.filter_video => C![C.song_sort_button_selected]],
ev(Ev::Click, |_| Msg::Shuffle),
span![C![C.tooltiptext], "Blanda låtar"],
"🔀",
],
],
div![
C![C.song_list],
attrs! {At::Id => SONG_LIST_ID},
ev(Ev::Scroll, |_| Msg::Scroll),
model
.filtered_songs
.iter()
.map(|&i| &model.songs[i])
.map(song_card)
.take(model.filtered_songs.len() - model.hidden_songs)
.take(model.shown_songs),
],
]
}
const SONG_LIST_ID: &str = "song_list";
fn get_song_list_element() -> anyhow::Result<Element> {
document()
.get_element_by_id(SONG_LIST_ID)
.ok_or(anyhow!("Failed to access song list element"))
}
fn scroll_to_top() {
if let Ok(elem) = get_song_list_element() {
elem.scroll_to_with_x_and_y(0.0, 0.0);
}
}
fn get_scroll() -> anyhow::Result<(i32, i32)> {
let list = get_song_list_element()?;
let scroll = list.scroll_top();
let height = list.client_height();
let max = (list.scroll_height() - height).max(0);
Ok((scroll, max))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB