Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deno KV cache #63

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: denoland/setup-deno@v1
- name: Compile x86_64
run: |
deno compile --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi --target x86_64-apple-darwin music-rpc.ts
deno compile --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi --unstable-kv --target x86_64-apple-darwin music-rpc.ts
mv music-rpc music-rpc-${{ steps.version.outputs.commit_sha }}-x86_64-apple-darwin
- name: Upload x86_64
uses: actions/upload-artifact@v4
Expand All @@ -27,7 +27,7 @@ jobs:
path: music-rpc-${{ steps.version.outputs.commit_sha }}-x86_64-apple-darwin
- name: Compile aarch64
run: |
deno compile --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi --target aarch64-apple-darwin music-rpc.ts
deno compile --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi --unstable-kv --target aarch64-apple-darwin music-rpc.ts
mv music-rpc music-rpc-${{ steps.version.outputs.commit_sha }}-aarch64-apple-darwin
- name: Upload aarch64
uses: actions/upload-artifact@v4
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
.DS_Store
/cache_v?.sqlite3*
/music-rpc.log
/cache.json
120 changes: 41 additions & 79 deletions music-rpc.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,28 @@
#!/usr/bin/env deno run --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi

#!/usr/bin/env deno run --allow-env --allow-run --allow-net --allow-read --allow-write --unstable-ffi --allow-ffi --unstable-kv
import type { Activity } from "https://deno.land/x/[email protected]/mod.ts";
import { Client } from "https://deno.land/x/[email protected]/mod.ts";
import type {} from "https://raw.githubusercontent.com/NextFire/jxa/v0.0.5/run/global.d.ts";
import { run } from "https://raw.githubusercontent.com/NextFire/jxa/v0.0.5/run/mod.ts";
import type { iTunes } from "https://raw.githubusercontent.com/NextFire/jxa/v0.0.5/run/types/core.d.ts";

// Cache

class Cache {
static VERSION = 5;
static CACHE_FILE = "cache.json";
static #data: Map<string, TrackExtras> = new Map();

static get(key: string) {
return this.#data.get(key);
}

static set(key: string, value: TrackExtras) {
this.#data.set(key, value);
this.saveCache();
}

static async loadCache() {
try {
const text = await Deno.readTextFile(this.CACHE_FILE);
const data = JSON.parse(text);
if (data.version !== this.VERSION) throw new Error("Old cache");
this.#data = new Map(data.data);
} catch (err) {
console.error(
err,
`No valid ${this.CACHE_FILE} found, generating a new cache...`
);
}
}

static async saveCache() {
try {
await Deno.writeTextFile(
this.CACHE_FILE,
JSON.stringify({
version: this.VERSION,
data: Array.from(this.#data.entries()),
})
);
} catch (err) {
console.error(err);
}
}
}

// Main part

//#region entrypoint
const MACOS_VER = await getMacOSVersion();
const IS_APPLE_MUSIC = MACOS_VER >= 10.15;
const APP_NAME: iTunesAppName = IS_APPLE_MUSIC ? "Music" : "iTunes";
const CLIENT_ID = IS_APPLE_MUSIC ? "773825528921849856" : "979297966739300416";
const DEFAULT_TIMEOUT = 15e3;

start();
const KV_VERSION = 0;
const kv = await Deno.openKv(`cache_v${KV_VERSION}.sqlite3`);

async function start() {
await Cache.loadCache();
const rpc = new Client({ id: CLIENT_ID });
while (true) {
try {
await main(rpc);
} catch (err) {
console.error(err);
await new Promise((resolve) => setTimeout(resolve, DEFAULT_TIMEOUT));
}
const DEFAULT_TIMEOUT = 15e3;

const rpc = new Client({ id: CLIENT_ID });
while (true) {
try {
await main(rpc);
} catch (err) {
console.error(err);
await sleep(DEFAULT_TIMEOUT);
}
}

Expand All @@ -79,12 +31,16 @@ async function main(rpc: Client) {
console.log(rpc);
while (true) {
const timeout = await setActivity(rpc);
await new Promise((resolve) => setTimeout(resolve, timeout));
await sleep(timeout);
}
}

// macOS/JXA functions
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
//#endregion

//#region macOS/JXA functions
async function getMacOSVersion(): Promise<number> {
const cmd = new Deno.Command("sw_vers", { args: ["-productVersion"] });
const output = await cmd.output();
Expand Down Expand Up @@ -115,22 +71,23 @@ function getProps(): Promise<iTunesProps> {
};
}, APP_NAME);
}
//#endregion

//#region iTunes Search API
async function getTrackExtras(props: iTunesProps): Promise<TrackExtras> {
const { name, artist, album } = props;
const cacheIndex = `${name} ${artist} ${album}`;
let infos = Cache.get(cacheIndex);
const entry = await kv.get<TrackExtras>(["extras", cacheIndex]);
let infos = entry.value;

if (!infos) {
infos = await _getTrackExtras(name, artist, album);
Cache.set(cacheIndex, infos);
await kv.set(["extras", cacheIndex], infos);
}

return infos;
}

// iTunes Search API

async function _getTrackExtras(
song: string,
artist: string,
Expand Down Expand Up @@ -174,20 +131,16 @@ async function _getTrackExtras(
const iTunesUrl = result?.trackViewUrl ?? null;
return { artworkUrl, iTunesUrl };
}
//#endregion

// MusicBrainz Artwork Getter

const MB_EXCLUDED_NAMES = ["", "Various Artist"];
const luceneEscape = (term: string) =>
term.replace(/([+\-&|!(){}\[\]^"~*?:\\])/g, "\\$1");
const removeParenthesesContent = (term: string) =>
term.replace(/\([^)]*\)/g, "").trim();

//#region MusicBrainz
async function _getMBArtwork(
artist: string,
song: string,
album: string
): Promise<string | undefined> {
const MB_EXCLUDED_NAMES = ["", "Various Artist"];

const queryTerms = [];
if (!MB_EXCLUDED_NAMES.every((elem) => artist.includes(elem))) {
queryTerms.push(
Expand Down Expand Up @@ -226,8 +179,16 @@ async function _getMBArtwork(
return result;
}

// Activity setter
function luceneEscape(term: string): string {
return term.replace(/([+\-&|!(){}\[\]^"~*?:\\])/g, "\\$1");
}

function removeParenthesesContent(term: string): string {
return term.replace(/\([^)]*\)/g, "").trim();
}
//#endregion

//#region Activity setter
async function setActivity(rpc: Client): Promise<number> {
const open = await isOpen();
console.log("isOpen:", open);
Expand Down Expand Up @@ -321,14 +282,14 @@ async function setActivity(rpc: Client): Promise<number> {
* @param maxLength
* @returns Formatted string
*/
function formatStr(s: string, minLength = 2, maxLength = 128) {
function formatStr(s: string, minLength = 2, maxLength = 128): string {
return s.length <= maxLength
? s.padEnd(minLength)
: `${s.slice(0, maxLength - 3)}...`;
}
//#endregion

// TypeScript

//#region TypeScript
type iTunesAppName = "iTunes" | "Music";

interface iTunesProps {
Expand Down Expand Up @@ -365,3 +326,4 @@ interface MBReleaseLookupResponse {
interface MBRelease {
id: string;
}
//#endregion
Loading