Compare commits
7 Commits
3800b448e1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f42985733f
|
|||
|
c6cc83018f
|
|||
|
89cb0c475b
|
|||
|
95dbb74b47
|
|||
|
a894717e52
|
|||
|
1092860b42
|
|||
|
ec5daba300
|
5
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/dist
|
||||
target
|
||||
dist
|
||||
.env
|
||||
|
||||
1855
Cargo.lock
generated
23
Cargo.toml
@ -1,20 +1,3 @@
|
||||
[package]
|
||||
name = "singit2"
|
||||
version = "0.2.2"
|
||||
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"
|
||||
|
||||
44
Dockerfile
@ -1,11 +1,12 @@
|
||||
##################
|
||||
### BASE STAGE ###
|
||||
##################
|
||||
FROM rust:1.60 as base
|
||||
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
|
||||
@ -16,6 +17,9 @@ RUN mkdir frontend backend common
|
||||
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
|
||||
|
||||
###################
|
||||
@ -23,18 +27,50 @@ RUN strip_cargo_version
|
||||
###################
|
||||
FROM base AS build
|
||||
|
||||
RUN cargo init --lib .
|
||||
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 nginx:alpine
|
||||
FROM scratch
|
||||
|
||||
COPY --from=base /app/dist/* /usr/share/nginx/html/
|
||||
# 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
@ -0,0 +1 @@
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
2014
backend/Cargo.lock
generated
Normal file
17
backend/Cargo.toml
Normal 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
@ -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
@ -0,0 +1 @@
|
||||
../frontend/dist
|
||||
0
backend/migrations/.keep
Normal 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();
|
||||
@ -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;
|
||||
@ -0,0 +1,2 @@
|
||||
DROP TABLE song;
|
||||
|
||||
13
backend/migrations/2023-09-21-174215_add_song_table/up.sql
Normal 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
|
||||
);
|
||||
@ -0,0 +1,2 @@
|
||||
DROP TABLE custom_list_entry;
|
||||
DROP TABLE custom_list;
|
||||
10
backend/migrations/2023-09-21-202738_add_custom_lists/up.sql
Normal 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)
|
||||
);
|
||||
253
backend/mock/custom_lists.sql
Normal 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
23
backend/src/db.rs
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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(())
|
||||
}
|
||||
@ -1,45 +1,81 @@
|
||||
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 anyhow::anyhow;
|
||||
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, error, img, input, p, span, C, IF};
|
||||
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)>,
|
||||
|
||||
/// The search string
|
||||
/// Custom song lists, lazily loaded.
|
||||
custom_lists: CustomLists,
|
||||
|
||||
/// The search string.
|
||||
query: String,
|
||||
|
||||
/// The number of songs currently in view. Goes up when the user scrolls down.
|
||||
/// 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
|
||||
/// The number of songs that didn't match the search critera.
|
||||
hidden_songs: usize,
|
||||
|
||||
/// Whether we're filtering by video
|
||||
/// Whether we're filtering by video.
|
||||
filter_video: bool,
|
||||
|
||||
/// Whether we're filtering by duets
|
||||
/// 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),
|
||||
|
||||
@ -60,15 +96,16 @@ pub enum Msg {
|
||||
}
|
||||
|
||||
pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
let mut songs: Vec<Song> =
|
||||
serde_json::from_str(include_str!("../static/songs.json")).expect("parse songs");
|
||||
songs.shuffle(&mut thread_rng());
|
||||
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: songs
|
||||
.into_iter()
|
||||
.map(|song| (Default::default(), song))
|
||||
.collect(),
|
||||
songs: vec![],
|
||||
custom_lists: Default::default(),
|
||||
query: String::new(),
|
||||
hidden_songs: 0,
|
||||
shown_songs: INITIAL_ELEM_COUNT,
|
||||
@ -77,39 +114,72 @@ pub fn init(_url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
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::Search(query) => {
|
||||
model.hidden_songs = 0;
|
||||
model.shown_songs = INITIAL_ELEM_COUNT;
|
||||
scroll_to_top();
|
||||
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.query = query;
|
||||
*model.custom_lists.entry(list).or_default() = Loading::Loaded(song_hashes);
|
||||
|
||||
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);
|
||||
|
||||
// calculate search scores & sort list
|
||||
for (score, song) in model.songs.iter_mut() {
|
||||
let new_score = song.fuzzy_compare(&query);
|
||||
if new_score < Default::default() {
|
||||
model.hidden_songs += 1;
|
||||
}
|
||||
|
||||
*score = Reverse(new_score);
|
||||
}
|
||||
model.songs.sort_unstable();
|
||||
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 {
|
||||
@ -135,12 +205,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
autotype_song(model, orders);
|
||||
}
|
||||
Msg::Scroll => {
|
||||
let (scroll, max_scroll) = match get_scroll() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(e);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
@ -171,6 +238,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
|
||||
let song_card = |song: &Song| -> Node<Msg> {
|
||||
div![
|
||||
C![C.song_item],
|
||||
@ -178,7 +246,16 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
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"},
|
||||
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![
|
||||
@ -284,6 +361,20 @@ pub fn view(model: &Model) -> Vec<Node<Msg>> {
|
||||
]
|
||||
}
|
||||
|
||||
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();
|
||||
@ -293,22 +384,20 @@ pub fn autotype_song(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
|
||||
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_else(|| anyhow!("Failed to access song list element"))
|
||||
fn get_song_list_element() -> Option<Element> {
|
||||
document().get_element_by_id(SONG_LIST_ID)
|
||||
}
|
||||
|
||||
fn scroll_to_top() {
|
||||
if let Ok(elem) = get_song_list_element() {
|
||||
if let Some(elem) = get_song_list_element() {
|
||||
elem.scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scroll() -> anyhow::Result<(i32, i32)> {
|
||||
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);
|
||||
Ok((scroll, max))
|
||||
Some((scroll, max))
|
||||
}
|
||||
@ -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");
|
||||
34
frontend/src/custom_list.rs
Normal 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
@ -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)),
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -30,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> {
|
||||
@ -50,6 +53,7 @@ impl<'a> ParsedQuery<'a> {
|
||||
"lang" => parsed.language = Some(v),
|
||||
"genre" => parsed.genre = Some(v),
|
||||
"year" => parsed.year = Some(v),
|
||||
"list" => parsed.list = Some(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -59,8 +63,7 @@ impl<'a> ParsedQuery<'a> {
|
||||
|
||||
/// 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.trim().split_whitespace().next().unwrap_or("") };
|
||||
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();
|
||||
@ -119,8 +122,8 @@ impl<'a> ParsedQuery<'a> {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -137,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)> {
|
||||
@ -173,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(())
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
use crate::app::Loading;
|
||||
use crate::custom_list::CustomLists;
|
||||
use crate::fuzzy::{self, FuzzyScore};
|
||||
use crate::query::ParsedQuery;
|
||||
use serde::Deserialize;
|
||||
@ -27,8 +29,8 @@ 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 {
|
||||
@ -56,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();
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
BIN
frontend/static/images/default_covers/00.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/01.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/02.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/03.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/04.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/05.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/06.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/07.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/08.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/09.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/10.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/11.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/12.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/13.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/14.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/15.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/16.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/images/default_covers/17.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/18.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/19.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/20.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/21.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/22.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/23.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/static/images/default_covers/24.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |