From 68e47894619a842820ea0537d3f2cf3d502c3be7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= <t@mldk.cz>
Date: Sun, 1 Oct 2023 12:56:13 +0200
Subject: [PATCH] wip: async loading of both album sources

---
 src-tauri/Cargo.lock  | 423 ++++++++++++++++++++++++++++++++----------
 src-tauri/Cargo.toml  |  22 +--
 src-tauri/src/main.rs | 127 ++++++++++++-
 src/App.svelte        | 189 ++++++++++++++++---
 src/lib/Greet.svelte  |  27 ---
 src/stores.ts         |  17 ++
 src/styles.css        |   1 +
 7 files changed, 633 insertions(+), 173 deletions(-)
 delete mode 100644 src/lib/Greet.svelte
 create mode 100644 src/stores.ts

diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 4080ae1..d70b056 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -86,26 +86,6 @@ dependencies = [
  "system-deps 6.1.1",
 ]
 
-[[package]]
-name = "audiotags"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f47bdf1acc4fa4113f8aa72bb3bfe62a61443c61d0d997b723ba61ce102c79b"
-dependencies = [
- "audiotags-dev-macro",
- "id3",
- "metaflac",
- "mp4ameta",
- "readme-rustdocifier",
- "thiserror",
-]
-
-[[package]]
-name = "audiotags-dev-macro"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b79298591161f312f06327df7963063ee07466be303dcc3084a44ec293cb36e"
-
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -439,30 +419,6 @@ dependencies = [
  "crossbeam-utils",
 ]
 
-[[package]]
-name = "crossbeam-deque"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
-dependencies = [
- "cfg-if",
- "crossbeam-epoch",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
-dependencies = [
- "autocfg",
- "cfg-if",
- "crossbeam-utils",
- "memoffset",
- "scopeguard",
-]
-
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.16"
@@ -634,12 +590,6 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
 
-[[package]]
-name = "either"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
-
 [[package]]
 name = "embed-resource"
 version = "2.3.0"
@@ -650,7 +600,7 @@ dependencies = [
  "rustc_version",
  "toml 0.7.8",
  "vswhom",
- "winreg",
+ "winreg 0.51.0",
 ]
 
 [[package]]
@@ -825,6 +775,12 @@ dependencies = [
  "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"
@@ -1151,6 +1107,25 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "h2"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap 1.9.3",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.12.3"
@@ -1229,12 +1204,72 @@ dependencies = [
  "itoa 1.0.9",
 ]
 
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "http-range"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
 
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 1.0.9",
+ "pin-project-lite",
+ "socket2 0.4.9",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.57"
@@ -1365,6 +1400,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
+
 [[package]]
 name = "itoa"
 version = "0.4.8"
@@ -1497,10 +1538,8 @@ checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
 name = "lissen"
 version = "0.0.0"
 dependencies = [
- "anyhow",
- "audiotags",
  "id3",
- "rayon",
+ "reqwest",
  "serde",
  "serde_json",
  "tauri",
@@ -1613,15 +1652,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "metaflac"
-version = "0.2.5"
+name = "mime"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1470d3cc1bb0d692af5eb3afb594330b8ba09fd91c32c4e1c6322172a5ba750"
-dependencies = [
- "byteorder",
- "hex",
- "log",
-]
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
 name = "miniz_oxide"
@@ -1634,20 +1668,33 @@ dependencies = [
 ]
 
 [[package]]
-name = "mp4ameta"
-version = "0.11.0"
+name = "mio"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
- "lazy_static",
- "mp4ameta_proc",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
-name = "mp4ameta_proc"
-version = "0.6.0"
+name = "native-tls"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
 
 [[package]]
 name = "ndk"
@@ -1824,6 +1871,50 @@ dependencies = [
  "windows-sys 0.42.0",
 ]
 
+[[package]]
+name = "openssl"
+version = "0.10.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
+dependencies = [
+ "bitflags 2.4.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -2209,32 +2300,6 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
 
-[[package]]
-name = "rayon"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
-dependencies = [
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
-dependencies = [
- "crossbeam-deque",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "readme-rustdocifier"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac"
-
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -2308,6 +2373,43 @@ version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
 
+[[package]]
+name = "reqwest"
+version = "0.11.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
+dependencies = [
+ "base64 0.21.4",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg 0.50.0",
+]
+
 [[package]]
 name = "rfd"
 version = "0.10.0"
@@ -2387,6 +2489,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.1"
@@ -2399,6 +2510,29 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "selectors"
 version = "0.22.0"
@@ -2479,6 +2613,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.9",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "serde_with"
 version = "3.3.0"
@@ -2587,6 +2733,26 @@ version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
 
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "soup2"
 version = "0.2.1"
@@ -3084,8 +3250,36 @@ checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
 dependencies = [
  "backtrace",
  "bytes",
+ "libc",
+ "mio",
  "num_cpus",
  "pin-project-lite",
+ "socket2 0.5.4",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
 ]
 
 [[package]]
@@ -3131,6 +3325,12 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
 [[package]]
 name = "tracing"
 version = "0.1.37"
@@ -3202,6 +3402,12 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
 [[package]]
 name = "typenum"
 version = "1.17.0"
@@ -3268,6 +3474,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version-compare"
 version = "0.0.11"
@@ -3316,6 +3528,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
@@ -3780,6 +4001,16 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "winreg"
 version = "0.51.0"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 727ccfa..f8a8091 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,26 +1,24 @@
 [package]
-name = "lissen"
-version = "0.0.0"
-description = "A Tauri App"
 authors = ["you"]
-license = ""
-repository = ""
+description = "A Tauri App"
 edition = "2021"
+license = ""
+name = "lissen"
+repository = ""
+version = "0.0.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [build-dependencies]
-tauri-build = { version = "1.4", features = [] }
+tauri-build = {version = "1.4", features = [] }
 
 [dependencies]
-tauri = { version = "1.4", features = [ "dialog-open", "shell-open"] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
 id3 = "1.8.0"
-rayon = "1.8.0"
+reqwest = {version = "0.11.20", features = ["json"] }
+serde = {version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tauri = {version = "1.4", features = ["dialog-open", "shell-open"] }
 walkdir = "2.4.0"
-anyhow = "1.0.75"
-audiotags = "0.4.1"
 
 [features]
 # this feature is used for production builds or when `devPath` points to the filesystem
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 6bc5468..25de980 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -1,12 +1,13 @@
 // Prevents additional console window on Windows in release, DO NOT REMOVE!!
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 use id3::{Tag, TagLike};
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use std::collections::HashSet;
+use tauri::Manager;
 use walkdir::WalkDir;
 
-#[derive(Debug, Serialize, PartialEq, Eq, Hash)]
-pub struct Album {
+#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
+pub struct LocalAlbum {
     pub name: String,
     pub artist: String,
     pub year: i32,
@@ -14,15 +15,16 @@ pub struct Album {
 }
 
 #[tauri::command]
-async fn get_local_albums(dirpath: String) -> Result<Vec<Album>, String> {
+async fn get_local_albums(
+    dirpath: String,
+    app_handle: tauri::AppHandle,
+) -> Result<Vec<LocalAlbum>, String> {
     let mut result = HashSet::new();
-    for entry in WalkDir::new(dirpath) {
-        let entry = entry.map_err(|e| e.to_string())?;
-        println!("{}", entry.path().display());
+    for entry in WalkDir::new(dirpath).into_iter().filter_map(|e| e.ok()) {
         if entry.file_type().is_file() {
             let path = entry.path();
             if let Ok(tag) = Tag::read_from_path(path) {
-                let album = Album {
+                let album = LocalAlbum {
                     name: tag.album().unwrap_or("").to_string(),
                     artist: tag.artist().unwrap_or("").to_string(),
                     year: tag.year().unwrap_or(0),
@@ -32,19 +34,124 @@ async fn get_local_albums(dirpath: String) -> Result<Vec<Album>, String> {
                         .map(|ext| ext == "flac" || ext == "wav")
                         .unwrap_or(false),
                 };
-                result.insert(album);
+                let json_local_album = serde_json::to_value(&album).unwrap();
+                let new = result.insert(album);
+                if new {
+                    app_handle
+                        .emit_all("new_local_album", json_local_album)
+                        .unwrap();
+                }
             }
         }
     }
     Ok(result.into_iter().collect())
 }
 
+#[derive(Debug, Serialize, PartialEq, Eq, Hash)]
+pub struct ScrobbledAlbum {
+    pub name: String,
+    pub artist: String,
+    pub playcount: u64,
+}
+
+#[derive(Debug, Deserialize)]
+pub enum Period {
+    Overall,
+    Week,
+    Month,
+    ThreeMonths,
+    SixMonths,
+    Year,
+}
+
+impl ToString for Period {
+    fn to_string(&self) -> String {
+        match self {
+            Period::Overall => "overall",
+            Period::Week => "7day",
+            Period::Month => "1month",
+            Period::ThreeMonths => "3month",
+            Period::SixMonths => "6month",
+            Period::Year => "12month",
+        }
+        .to_string()
+    }
+}
+
+#[tauri::command]
+async fn get_top_scrobbled_albums(
+    user: String,
+    period: Period,
+    limit: u64,
+    api_key: Option<String>,
+    app_handle: tauri::AppHandle,
+) -> Result<Vec<ScrobbledAlbum>, String> {
+    let api_key = api_key
+        .or_else(|| option_env!("LASTFM_API_KEY").map(|s| s.to_string()))
+        .ok_or("No API key supplied.")?;
+
+    let period = period.to_string();
+
+    let page_size = 50;
+    let mut final_result = Vec::new();
+    let mut page = 1;
+    while final_result.len() < limit as usize {
+        let url = format!(
+            "https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user={user}&api_key={api_key}&period={period}&limit={page_size}&page={page}&format=json");
+        println!("Fetching {}", url);
+
+        let resp = reqwest::get(&url)
+            .await
+            .map_err(|e| e.to_string())?
+            .json::<serde_json::Value>()
+            .await
+            .map_err(|e| e.to_string())?;
+
+        let albums = resp["topalbums"]["album"]
+            .as_array()
+            .ok_or("no albums found")?;
+
+        let mut result = Vec::new();
+        for album in albums {
+            let name = album["name"].as_str().ok_or("no album name")?.to_string();
+            let artist = album["artist"]["name"]
+                .as_str()
+                .ok_or("no artist name")?
+                .to_string();
+            let playcount = album["playcount"]
+                .as_str()
+                .ok_or("no playcount")?
+                .parse()
+                .map_err(|_| "failed to parse playcount")?;
+            result.push(ScrobbledAlbum {
+                name,
+                artist,
+                playcount,
+            });
+        }
+        app_handle
+            .emit_all(
+                "new_scrobbled_albums",
+                serde_json::to_value(&result).unwrap(),
+            )
+            .unwrap();
+        final_result.extend(result);
+
+        page += 1;
+    }
+
+    Ok(final_result)
+}
+
 fn main() {
     // https://github.com/tauri-apps/tauri/issues/5143#issuecomment-1311815517
     std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
 
     tauri::Builder::default()
-        .invoke_handler(tauri::generate_handler![get_local_albums])
+        .invoke_handler(tauri::generate_handler![
+            get_top_scrobbled_albums,
+            get_local_albums
+        ])
         .run(tauri::generate_context!())
         .expect("error while running tauri application");
 }
diff --git a/src/App.svelte b/src/App.svelte
index f7506fc..b76517a 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,39 +1,172 @@
 <script lang="ts">
-  import Greet from './lib/Greet.svelte'
+import { invoke } from "@tauri-apps/api/tauri";
+import { open } from "@tauri-apps/api/dialog";
+import { listen } from "@tauri-apps/api/event";
+import { onMount } from "svelte";
+import { period, username } from "./stores";
+
+interface ScrobbledAlbum {
+  name: string;
+  artist: string;
+  playcount: number;
+}
+let scrobbledAlbums: ScrobbledAlbum[] = [];
+let loadingScrobbledAlbums = false;
+
+interface LocalAlbum {
+  name: string;
+  artist: string;
+  year: number;
+  lossless: boolean;
+}
+let localAlbums: LocalAlbum[] = [];
+let loadingLocalAlbums = false;
+
+onMount(() => {
+  listen("new_local_album", (event) => {
+    const album = event.payload as LocalAlbum;
+    localAlbums = [...localAlbums, album];
+  });
+  listen("new_scrobbled_albums", (event) => {
+    const newAlbums = event.payload as ScrobbledAlbum[];
+    scrobbledAlbums = [...scrobbledAlbums, ...newAlbums];
+  });
+});
+
+async function loadScrobbles() {
+  if (loadingScrobbledAlbums) {
+    return;
+  }
+  scrobbledAlbums = [];
+  loadingScrobbledAlbums = true;
+  try {
+    await invoke("get_top_scrobbled_albums", {
+      user: $username,
+      period: $period,
+      limit: 200,
+    });
+  } catch (e) {
+    handleError(e);
+  }
+  loadingScrobbledAlbums = false;
+}
+
+async function loadLocal() {
+  const selected = await open({
+    directory: true,
+  });
+  if (typeof selected === "string" && selected.length > 0) {
+    localAlbums = [];
+    loadingLocalAlbums = true;
+    try {
+      await invoke("get_local_albums", {
+        dirpath: selected,
+      });
+    } catch (e) {
+      handleError(e);
+    }
+    loadingLocalAlbums = false;
+  }
+}
+
+function handleError(e: unknown) {
+  console.error(e);
+  alert(e);
+}
 </script>
 
-<main class="container">
-  <h1>Welcome to Tauri!</h1>
+<main>
+  <div class="main">
+    <div class="scrobbles">
+      <div class="input">
+        <label>
+          Username:
+          <input type="text" bind:value={$username} />
+        </label>
+        <label>
+          Period:
+          <select bind:value={$period}>
+            <option value="Week">7 days</option>
+            <option value="Month">1 month</option>
+            <option value="ThreeMonths">3 months</option>
+            <option value="SixMonths">6 months</option>
+            <option value="Year">Year</option>
+            <option value="Overall">Overall</option>
+          </select>
+        </label>
 
-  <div class="row">
-    <a href="https://vitejs.dev" target="_blank">
-      <img src="/vite.svg" class="logo vite" alt="Vite Logo" />
-    </a>
-    <a href="https://tauri.app" target="_blank">
-      <img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
-    </a>
-    <a href="https://svelte.dev" target="_blank">
-      <img src="/svelte.svg" class="logo svelte" alt="Svelte Logo" />
-    </a>
+        <button on:click={loadScrobbles}>Load scrobbles</button>
+      </div>
+
+      <div class="display">
+        <div class="label">Scrobbled albums: {scrobbledAlbums.length}</div>
+        <div class="albums">
+          {#each scrobbledAlbums as album}
+            <div class="album">
+              <div class="name">{album.name}</div>
+              <div class="artist">{album.artist}</div>
+              <div class="playcount">Played: {album.playcount}</div>
+            </div>
+          {/each}
+        </div>
+      </div>
+    </div>
+    <div class="local">
+      <div class="input">
+        <button on:click={loadLocal}>Load local albums</button>
+      </div>
+      <div class="albums">
+        {#each localAlbums as album}
+          <div class="album">
+            <div class="name">{album.name}</div>
+            <div class="artist">{album.artist}</div>
+            <div class="year">{album.year}</div>
+            <div class="lossless">{album.lossless}</div>
+          </div>
+        {/each}
+      </div>
+    </div>
   </div>
-
-  <p>
-    Click on the Tauri, Vite, and Svelte logos to learn more.
-  </p>
-
-  <div class="row">
-    <Greet />
-  </div>
-
-
 </main>
 
 <style>
-  .logo.vite:hover {
-    filter: drop-shadow(0 0 2em #747bff);
+main {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.main {
+  display: flex;
+  width: 100vw;
+}
+
+.main > * {
+  flex-basis: 50%;
+}
+
+.albums {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+  margin: 1rem;
+}
+
+.album {
+  display: flex;
+  flex-direction: column;
+  border: 1px solid white;
+  border-radius: 2px;
+  padding: 0.5rem;
+}
+
+.album {
+  & .name {
+    font-weight: bold;
   }
 
-  .logo.svelte:hover {
-    filter: drop-shadow(0 0 2em #ff3e00);
+  & .artist {
+    font-size: 0.8em;
   }
-</style>
\ No newline at end of file
+}
+</style>
diff --git a/src/lib/Greet.svelte b/src/lib/Greet.svelte
deleted file mode 100644
index 75c6760..0000000
--- a/src/lib/Greet.svelte
+++ /dev/null
@@ -1,27 +0,0 @@
-<script lang="ts">
-import { invoke } from "@tauri-apps/api/tauri";
-import { open } from "@tauri-apps/api/dialog";
-
-let name = "";
-let greetMsg = "";
-
-async function greet() {
-  const selected = await open({
-    directory: true,
-  });
-  if (typeof selected === "string" && selected.length > 0) {
-    const res = await invoke("get_local_albums", {
-      dirpath: selected,
-    });
-    console.log({ res });
-  }
-  greetMsg = selected.toString();
-}
-</script>
-
-<div>
-  <form class="row" on:submit|preventDefault={greet}>
-    <button type="submit">Load</button>
-  </form>
-  <p>{greetMsg}</p>
-</div>
diff --git a/src/stores.ts b/src/stores.ts
new file mode 100644
index 0000000..73f181b
--- /dev/null
+++ b/src/stores.ts
@@ -0,0 +1,17 @@
+import { writable } from "svelte/store";
+
+export const username = writable(localStorage.getItem("username"));
+username.subscribe((value) => (localStorage.username = value));
+
+export type Period =
+  | "Overall"
+  | "Week"
+  | "Month"
+  | "ThreeMonths"
+  | "SixMonths"
+  | "Year";
+
+export const period = writable<Period>(
+  (localStorage.getItem("period") as Period) || "Overall"
+);
+period.subscribe((value) => (localStorage.period = value));
diff --git a/src/styles.css b/src/styles.css
index f7de85b..f49fc47 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -54,6 +54,7 @@ h1 {
 }
 
 input,
+select,
 button {
   border-radius: 8px;
   border: 1px solid transparent;