From af741f029db218835ed61355f906249b88246080 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 19:58:27 -0500 Subject: [PATCH 01/13] Clean up & standardize launchd plist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * launchd agents & daemons are conventionally named in reverse domain format * Move $PATH into the plist * UserName isn’t needed for a User-level launchd agent (i.e., one that goes in ~/Library/LaunchAgents) * StartCalendarInterval obviates StartInterval * Use bash shebang in start.sh to simplify from ProgramArguments to Program --- README.md | 9 ++++++--- com.slgoetz.plexletterboxdsync.plist | 26 ++++++++++++++++++++++++++ launched.plexletterboxdsync.plist | 28 ---------------------------- start.sh | 2 +- 4 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 com.slgoetz.plexletterboxdsync.plist delete mode 100644 launched.plexletterboxdsync.plist mode change 100644 => 100755 start.sh diff --git a/README.md b/README.md index 34d5f7a..27bb5af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # plex-letterboxd + Syncs your letterboxd watchlist with movies on you plex ## Installation @@ -8,14 +9,16 @@ Syncs your letterboxd watchlist with movies on you plex 3. `yarn install` ## Usage -If you want this to run in the background and sync once a day you can add it as a launch script on your mac. To do so you will need to fill in the blanks (`{path-for-your-file}` and `{username}`) in `launched.plexletterboxdsync.plist`. Then check to see if you have a directory at `~/Library/LaunchAgents`. If not, create one and copy the launch file there. This should run at midnight every day and output a log file for synced movies as well as errors. -*Make sure the `PATH` set the start.sh file is replaced with your `PATH`* +If you want this to run in the background and sync once a day you can add it as a launch script on your mac. To do so you will need to fill in `{path-for-your-file}` in `com.slgoetz.plexletterboxdsync.plist`. Then check to see if you have a directory at `~/Library/LaunchAgents`. If not, create one and copy the launch file there. This should run at midnight every day and output a log file for synced movies as well as errors. + +*Make sure the `PATH` set in `~/Library/LaunchAgents/com.slgoetz.plexletterboxdsync.plist` is valid for your user* ### Run Once + If you would like to run this once, you can run with `node ./index.js` ## Roadmap + - [ ] Sync ratings - [ ] Sync Watched - diff --git a/com.slgoetz.plexletterboxdsync.plist b/com.slgoetz.plexletterboxdsync.plist new file mode 100644 index 0000000..2af6661 --- /dev/null +++ b/com.slgoetz.plexletterboxdsync.plist @@ -0,0 +1,26 @@ + + + + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin + + Label + com.slgoetz.plexletterboxdsync + Program + start.sh + StandardErrorPath + plex-letterboxd-sync-error.log + StandardOutPath + plex-letterboxd-sync.log + StartCalendarInterval + + Minute + 0 + + WorkingDirectory + /path/to/plex-letterboxd-sync + + diff --git a/launched.plexletterboxdsync.plist b/launched.plexletterboxdsync.plist deleted file mode 100644 index 12963dc..0000000 --- a/launched.plexletterboxdsync.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Label - launched.plexletterboxdsync - ProgramArguments - - sh - start.sh - - StandardErrorPath - /{path-for-your-file}/plex-letterboxd-sync/logger-error.text - StandardOutPath - /{path-for-your-file}/plex-letterboxd-sync/logger.text - StartCalendarInterval - - Minute - 0 - - StartInterval - 0 - UserName - {username} - WorkingDirectory - /{path-for-your-file}/plex-letterboxd-sync - - diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 2567310..4bf5dd9 --- a/start.sh +++ b/start.sh @@ -1,4 +1,4 @@ -export PATH=/opt/homebrew/opt/node@14/bin:/opt/homebrew/opt/node@14/bin:/opt/homebrew/opt/node@16/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin +#!/usr/bin/env bash yarn install node ./index.js From 33a5f9de58938f66dc98879082e46427d773c47a Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:11:02 -0500 Subject: [PATCH 02/13] Add yarn sync command & use it in ./start.sh --- package.json | 3 ++- start.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e261f4c..c61a87d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Sync your Letterboxd watchlist with your Plex media server.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "sync": "node index.js" }, "author": "", "type": "module", diff --git a/start.sh b/start.sh index 4bf5dd9..52b31e6 100755 --- a/start.sh +++ b/start.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash yarn install -node ./index.js +yarn sync From 3a744a70b9363180f5d8bb15c670c9d718919833 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:11:52 -0500 Subject: [PATCH 03/13] Use more specific $PLEX_USER env var $USERNAME is close enough to $USER (which exists in every macOS shell) to be confusing, this avoids that. --- .env.sample | 2 +- index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index f64d1a1..f7a197b 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,3 @@ PLEX_TOKEN= PLEX_IP= -USERNAME= \ No newline at end of file +PLEX_USER= diff --git a/index.js b/index.js index 372146d..1374d51 100644 --- a/index.js +++ b/index.js @@ -53,7 +53,7 @@ const getAllLibraries = async () => { }; const letterboxd = async () => { - return await fetch(`https://letterboxd.com/${process.env.USERNAME}/watchlist/`) + return await fetch(`https://letterboxd.com/${process.env.PLEX_USER}/watchlist/`) .then((response) => response.text()) .then((html) => { let $ = cheerio.load(html); From acf0f4aa53f680796215697e453a57f034c8edf0 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:12:24 -0500 Subject: [PATCH 04/13] Add EditorConfig 3-space indentation is unusual, this ensures folks abide by it. --- .editorconfig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8e3e66e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 + +# 3 space indentation +[*.js] +indent_style = space +indent_size = 3 From 2ba1b0727f647028fbcd7e5d1508f3db0a15dcfd Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:12:49 -0500 Subject: [PATCH 05/13] yarn upgrade dotenv --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bac314e..e963c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -377,9 +377,9 @@ domutils@^3.0.1: domhandler "^5.0.1" dotenv@^16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== duplexer@~0.1.1: version "0.1.2" From 2a649e44006dfa10ed3e4dee016b62110b54452c Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:12:55 -0500 Subject: [PATCH 06/13] yarn upgrade node-fetch --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index e963c7a..37d8229 100644 --- a/yarn.lock +++ b/yarn.lock @@ -967,9 +967,9 @@ node-domexception@^1.0.0: integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== node-fetch@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" - integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA== + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" @@ -1493,9 +1493,9 @@ verror@1.10.0: extsprintf "^1.2.0" web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== xml2js@0.4.16: version "0.4.16" From 80145cc44123a867e1f5d49f28719aba7c754421 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Sat, 18 Nov 2023 20:14:05 -0500 Subject: [PATCH 07/13] Add missing slash for getLbMovieInfo() Otherwise you get URLs like this: FetchError: request to https://letterboxd.commovie/ failed --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 1374d51..468c2d4 100644 --- a/index.js +++ b/index.js @@ -67,7 +67,7 @@ const letterboxd = async () => { }; const getLbMovieInfo = async (movies) => { const getFilm = async ({ filmId, filmSlug }) => { - return await fetch(`https://letterboxd.com${filmSlug}`) + return await fetch(`https://letterboxd.com/${filmSlug}`) .then((response) => response.text()) .then((html) => { let $ = cheerio.load(html); From fdf9aac0d924853671158428ebf7326621763588 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Wed, 5 Jun 2024 09:53:59 -0400 Subject: [PATCH 08/13] Update README to clarify setup & automation Signed-off-by: Jeff Byrnes --- README.md | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 27bb5af..059196c 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,44 @@ Syncs your letterboxd watchlist with movies on you plex ## Installation -1. `git clone https://github.com/slgoetz/plex-letterboxd-sync.git` -2. `cd plex-letterboxd-sync` -3. `yarn install` +```bash +git clone https://github.com/slgoetz/plex-letterboxd-sync.git +cd plex-letterboxd-sync +yarn install +``` -## Usage +## Setup + +Copy `.env.sample` to `.env` and fill in the environment variables. -If you want this to run in the background and sync once a day you can add it as a launch script on your mac. To do so you will need to fill in `{path-for-your-file}` in `com.slgoetz.plexletterboxdsync.plist`. Then check to see if you have a directory at `~/Library/LaunchAgents`. If not, create one and copy the launch file there. This should run at midnight every day and output a log file for synced movies as well as errors. +* `$PLEX_TOKEN` - See [this Plex support article](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) +* `$PLEX_IP` - Can be an IP address (e.g., `192.168.1.2`) or a hostname (e.g., `plex.example.com`) +* `$PLEX_USER` - Your username with Plex (e.g., how you log in to plex.tv) -*Make sure the `PATH` set in `~/Library/LaunchAgents/com.slgoetz.plexletterboxdsync.plist` is valid for your user* +## Usage ### Run Once -If you would like to run this once, you can run with `node ./index.js` +If you would like to run this once: + +```bash +cd plex-letterboxd-sync +./start.sh +``` + +### Run On A Schedule + +If you want this to run in the background on a regular interval (e.g., once a day), you can add it as a launchd script on your Mac. + +1. Edit `com.slgoetz.plexletterboxdsync.plist` and replace `/path/to/plex-letterboxd-sync` with the absolute path to where you cloned this repository. E.g., `/Users/username/plex-letterboxd-sync` +2. *Make sure the `PATH` set in `~/Library/LaunchAgents/com.slgoetz.plexletterboxdsync.plist` is valid for your user,* it should ensure that `node` is available to launchd to run the script. + * E.g., if `node` is at `/usr/local/bin` or `/opt/homebrew/bin` (the default locations for Homebrew-installed Node.js, depending on if you have an Intel or Apple Silicon Mac), you should be good with the default value. +3. Check if you have a directory at `~/Library/LaunchAgents`. If not, create it. +4. Copy the launchd plist file, `com.slgoetz.plexletterboxdsync.plist`, to `~/Library/LaunchAgents` + +This will enable the script to run at midnight every day and output a log file for synced movies as well as errors to the `WorkingDirectory` you set in step 1 above. + +If you’d like to run it on a different schedule, the `StartCalendarInterval` stanza can be modified. You can do this by hand, or using a utility application like [LaunchControl](https://www.soma-zone.com/LaunchControl/). ## Roadmap From 281cb01c9ccf8f8e8d7bdd977e991be1cb12d221 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Wed, 5 Jun 2024 09:57:41 -0400 Subject: [PATCH 09/13] =?UTF-8?q?yarn=20upgrade=20xml2js=200.4.23=20?= =?UTF-8?q?=E2=86=92=200.6.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c61a87d..0f204c1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "node-schedule": "^2.1.1", "plex-api": "^5.3.2", "url-join": "^5.0.0", - "xml2js": "^0.4.23" + "xml2js": "^0.6.2" } } diff --git a/yarn.lock b/yarn.lock index 37d8229..0d1dad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1252,9 +1252,9 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + version "1.5.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.5.0.tgz#b5549b671069b7aa392df55ec7574cf411179eb8" + integrity sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA== set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" @@ -1513,10 +1513,10 @@ xml2js@0.4.19: sax ">=0.6.0" xmlbuilder "~9.0.1" -xml2js@^0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== +xml2js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== dependencies: sax ">=0.6.0" xmlbuilder "~11.0.0" From 3d622e0586367a1b68b519d36311c9e999a9b033 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Thu, 30 Apr 2026 20:44:52 -0400 Subject: [PATCH 10/13] Fix sync reliability: pagination, URL, year matching, error handling - Add LETTERBOXD_USER env var so Letterboxd and Plex usernames can differ - Fetch all pages of the Letterboxd watchlist (was silently capped at ~72 films) - Fix film detail URL to use /film// path (was hitting wrong endpoint) - Match by title AND year to avoid false positives from same-named remakes - Add try/catch in run() so failures exit with a non-zero code and a clear message - Remove dead code: getWatchList, getWatchListMovies, xmlToJSON, unused imports Co-Authored-By: Claude Sonnet 4.6 --- .env.sample | 1 + index.js | 212 ++++++++++++++++++++-------------------------------- 2 files changed, 84 insertions(+), 129 deletions(-) diff --git a/.env.sample b/.env.sample index f7a197b..fbfcbdb 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ PLEX_TOKEN= PLEX_IP= PLEX_USER= +LETTERBOXD_USER= diff --git a/index.js b/index.js index 468c2d4..f7be6dc 100644 --- a/index.js +++ b/index.js @@ -1,167 +1,121 @@ -// TODO: ratings -import xml2js from "xml2js"; import PlexAPI from "plex-api"; import fetch from "node-fetch"; import * as cheerio from "cheerio"; import dotenv from "dotenv"; -import _find from "lodash.find"; import logger from "log-to-file"; -// import schedule from 'node-schedule' dotenv.config(); -const optionDefinitions = [ - { name: "ip", type: String }, - { name: "listPath", type: String, multiple: false, defaultOption: true } -]; - const client = new PlexAPI({ hostname: process.env.PLEX_IP, - token: process.env.PLEX_TOKEN + token: process.env.PLEX_TOKEN, }); -const call = { - getAllLibraries: "/library/sections", - getWatchlist: "/library/watchlist", - getLllMovies: "/library/sections/{id}/all" - // getWatchlist: "/library/sections/watchlist/all", -}; - -const getWatchList = async () => { +const getAllLibraries = async () => { return client - .find(call.getWatchlist) - .then((res) => { - console.log(res); - }) + .find("/library/sections", { type: "movie" }) + .then((directories) => directories.map((lib) => lib.key)) .catch((err) => { - console.error("Could not connect to server", err); + throw new Error(`Could not fetch Plex libraries: ${err.message}`); }); }; -const getAllLibraries = async () => { - return client - .find(call.getAllLibraries, { type: "movie" }) - .then((directories) => { - const ids = directories.map((library) => { - return library.key; +const getAllMovies = async (libraries) => { + const getMoviesFromLib = (libID) => + client + .find(`/library/sections/${libID}/all`) + .catch((err) => { + throw new Error(`Could not fetch movies from library ${libID}: ${err.message}`); }); - return ids; - }) - .catch((err) => { - console.error("Could not connect to server", err); - }); + + return Promise.all(libraries.map(getMoviesFromLib)).then((res) => res.flat()); }; -const letterboxd = async () => { - return await fetch(`https://letterboxd.com/${process.env.PLEX_USER}/watchlist/`) - .then((response) => response.text()) - .then((html) => { - let $ = cheerio.load(html); - var films = []; - $(".poster-container").each((i, el) => { - const filmData = $(el).children().data(); - return films.push(filmData); - }); - return films; - }); +const fetchWatchlistPage = async (user, page) => { + const url = + page === 1 + ? `https://letterboxd.com/${user}/watchlist/` + : `https://letterboxd.com/${user}/watchlist/page/${page}/`; + + const html = await fetch(url).then((r) => r.text()); + const $ = cheerio.load(html); + + const films = []; + $(".poster-container").each((_, el) => { + films.push($(el).children().first().data()); + }); + + const lastPageLink = $(".paginate-pages a").last().attr("href") ?? ""; + const lastPageMatch = lastPageLink.match(/page\/(\d+)/); + const lastPage = lastPageMatch ? parseInt(lastPageMatch[1], 10) : 1; + + return { films, lastPage }; +}; + +const getLetterboxdWatchlist = async () => { + const user = process.env.LETTERBOXD_USER; + const { films, lastPage } = await fetchWatchlistPage(user, 1); + + if (lastPage === 1) return films; + + const remainingPages = Array.from({ length: lastPage - 1 }, (_, i) => i + 2); + const rest = await Promise.all( + remainingPages.map((page) => fetchWatchlistPage(user, page).then((r) => r.films)) + ); + + return [films, ...rest].flat(); }; + const getLbMovieInfo = async (movies) => { const getFilm = async ({ filmId, filmSlug }) => { - return await fetch(`https://letterboxd.com/${filmSlug}`) - .then((response) => response.text()) - .then((html) => { - let $ = cheerio.load(html); - const title = $("h1.headline-1").text().trim(); - const year = $(".film-header-lockup .number a").text(); - - return Object.assign({}, { filmId, filmSlug }, { title, year }); - }); + const html = await fetch(`https://letterboxd.com/film/${filmSlug}/`).then((r) => r.text()); + const $ = cheerio.load(html); + const title = $("h1.headline-1").text().trim(); + const year = parseInt($(".film-header-lockup .number a").text().trim(), 10); + return { filmId, filmSlug, title, year }; }; - const data = movies.map(async (film) => await getFilm(film)); - return Promise.all(data); -}; -const getWatchListMovies = async () => { - await fetch( - `https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=${process.env.PLEX_TOKEN}` - ) - .then((response) => response.text()) - .then((xmlString) => xmlToJSON(xmlString)) - .then((data) => console.log(data.MediaContainer.Video)); + return Promise.all(movies.map(getFilm)); }; -const syncWatchListMovies = async (plexMovies, LBMovies) => { - // get avilable Movies - const availMovies = plexMovies.filter((movie) => { - const match = _find(LBMovies, { title: movie.title }); - if (match) { - return match; - } - }); +const syncWatchListMovies = async (plexMovies, lbMovies) => { + const availMovies = plexMovies.filter((movie) => + lbMovies.some( + (lb) => lb.title === movie.title && lb.year === parseInt(movie.year, 10) + ) + ); - // Sync movies to Plex const syncMovie = async (movie) => { - const uuid = movie.guid.split("/"); - const ratingKey = uuid[uuid.length - 1]; - return await fetch( + const ratingKey = movie.guid.split("/").at(-1); + const res = await fetch( `https://metadata.provider.plex.tv/actions/addToWatchlist?X-Plex-Token=${process.env.PLEX_TOKEN}&ratingKey=${ratingKey}`, - { - method: "PUT" - } - ).then((res) => { - if (res.status === 200) { - const text = `SUCCESS - ${movie.title}`; - logger(text); - return text; - } else { - const text = `FAIL - ${movie.title}`; - logger(text); - return text; - } - }); + { method: "PUT" } + ); + const text = + res.status === 200 + ? `SUCCESS - ${movie.title}` + : `FAIL - ${movie.title} (HTTP ${res.status})`; + logger(text); + return text; }; - const data = availMovies.map(async (movie) => await syncMovie(movie)); - return Promise.all(data); -}; - -const getAllMovies = async (libraries) => { - const getMoviesFromLib = async (libID) => { - var url = call.getLllMovies.replace("{id}", libID); - return client - .find(url) - .then((movies) => movies) - .catch((err) => console.error("Could not connect to server", err)); - }; - - var data = libraries.map(async (library) => { - const libMovies = await getMoviesFromLib(library); - return libMovies; - }); - return Promise.all(data).then((res) => res.flat()); -}; - -// TODO:Move to Utils -const xmlToJSON = (str, options) => { - return new Promise((resolve, reject) => { - xml2js.parseString(str, options, (err, jsonObj) => { - if (err) { - return reject(err); - } - resolve(jsonObj); - }); - }); + return Promise.all(availMovies.map(syncMovie)); }; async function run() { - const libraries = await getAllLibraries(); - const plexMovies = await getAllMovies(libraries); - - // Get Data from LetterBoxd - const LBWatchList = await letterboxd(); - const LBMovies = await getLbMovieInfo(LBWatchList); - const watchlist = await syncWatchListMovies(plexMovies, LBMovies); + try { + const libraries = await getAllLibraries(); + const plexMovies = await getAllMovies(libraries); + + const lbWatchList = await getLetterboxdWatchlist(); + const lbMovies = await getLbMovieInfo(lbWatchList); + + const results = await syncWatchListMovies(plexMovies, lbMovies); + console.log(`Sync complete: ${results.length} movie(s) processed.`); + } catch (err) { + console.error("Sync failed:", err.message); + process.exit(1); + } } run(); -// schedule.scheduleJob("0 0 * * *", run); // run everyday at midnight From 3b558ca92b61223e3a825b03b6db7160913cc4ba Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Fri, 8 May 2026 11:46:41 -0400 Subject: [PATCH 11/13] Rename start script for clarity w/ launchd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script name is how launchd displays the automated task. This means in various macOS displays, the task will be listed as "start.sh", which isn’t very descriptive. By renaming it to "plex-letterboxd-sync", it becomes clear. --- start.sh => plex-letterboxd-sync | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename start.sh => plex-letterboxd-sync (100%) diff --git a/start.sh b/plex-letterboxd-sync similarity index 100% rename from start.sh rename to plex-letterboxd-sync From 0a13d1627f6b0e6c79ae1c4b79b3138c917cebfe Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Mon, 11 May 2026 12:12:16 -0400 Subject: [PATCH 12/13] Add missing Type log-to-file Missed in 3d622e0586367a1b68b519d36311c9e999a9b033 Signed-off-by: Jeff Byrnes --- types/log-to-file/index.d.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 types/log-to-file/index.d.ts diff --git a/types/log-to-file/index.d.ts b/types/log-to-file/index.d.ts new file mode 100644 index 0000000..a348cac --- /dev/null +++ b/types/log-to-file/index.d.ts @@ -0,0 +1 @@ +declare module 'log-to-file' From 39d5331d16ad7b0edbeb554e3a946a1ec03db791 Mon Sep 17 00:00:00 2001 From: Jeff Byrnes Date: Tue, 12 May 2026 18:50:59 -0400 Subject: [PATCH 13/13] yarn add dotenv@^17.4.2 Signed-off-by: Jeff Byrnes --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0f204c1..f70e4de 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.12", - "dotenv": "^16.0.3", + "dotenv": "^17.4.2", "forever-monitor": "^3.0.3", "lodash.find": "^4.6.0", "log-to-file": "^3.0.3", diff --git a/yarn.lock b/yarn.lock index 0d1dad2..a532ef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,10 +376,10 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.1" -dotenv@^16.0.3: - version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" - integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== +dotenv@^17.4.2: + version "17.4.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== duplexer@~0.1.1: version "0.1.2"