plexsonic 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +31 -1
- package/package.json +7 -6
- package/src/config.js +1 -0
- package/src/plex.js +39 -0
- package/src/server.js +956 -181
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ SESSION_SECRET=replace-with-a-long-random-secret
|
|
|
57
57
|
TOKEN_ENC_KEY=
|
|
58
58
|
PLEX_PRODUCT=Plexsonic Bridge
|
|
59
59
|
PLEX_CLIENT_IDENTIFIER=
|
|
60
|
+
PLEX_WEBHOOK_TOKEN=
|
|
60
61
|
LICENSE_EMAIL=
|
|
61
62
|
PLEX_INSECURE_TLS=0
|
|
62
63
|
LOG_LEVEL=warn
|
|
@@ -68,10 +69,11 @@ LOG_REQUESTS=0
|
|
|
68
69
|
- `PORT`: HTTP port (default `3127`).
|
|
69
70
|
- `BIND_HOST`: listen interface (`127.0.0.1` local only, `0.0.0.0` for LAN).
|
|
70
71
|
- `BASE_URL`: optional public URL override used for callback generation. If empty, origin is derived from request headers.
|
|
72
|
+
- `PLEX_WEBHOOK_TOKEN`: optional shared secret for `/webhooks/plex`. If set, webhook calls must provide this token.
|
|
71
73
|
- `SESSION_SECRET`: cookie/session signing secret. Keep stable across restarts.
|
|
72
74
|
- `TOKEN_ENC_KEY`: optional but recommended 32-byte key (hex or base64) used to encrypt stored Plex tokens.
|
|
73
75
|
- `LOG_LEVEL`: logger level (`trace`, `debug`, `info`, `warn`, `error`, `fatal`).
|
|
74
|
-
- `LOG_REQUESTS`: set to `1` to enable incoming request logs. Very verbose, and can expose
|
|
76
|
+
- `LOG_REQUESTS`: set to `1` to enable incoming request logs. Very verbose, and can expose login credentials. (`0` by default).
|
|
75
77
|
|
|
76
78
|
Generate secrets (examples):
|
|
77
79
|
|
|
@@ -166,6 +168,18 @@ Both endpoint styles are accepted:
|
|
|
166
168
|
- `/rest/getArtists.view`
|
|
167
169
|
- `/rest/getArtists`
|
|
168
170
|
|
|
171
|
+
### Star/Like Mapping (Plex)
|
|
172
|
+
|
|
173
|
+
Plexsonic maps Subsonic rating + star state into a single Plex numeric rating:
|
|
174
|
+
- Odd points = rated only (not liked): `1, 3, 5, 7, 9`
|
|
175
|
+
- Even points = liked: `2, 4, 6, 8, 10`
|
|
176
|
+
- `0` = no rating and not liked
|
|
177
|
+
|
|
178
|
+
Behavior:
|
|
179
|
+
- `setRating(r)` updates stars and keeps current like state when possible.
|
|
180
|
+
- `star` toggles like on and keeps star level. If unrated, it becomes `2` points (liked + 1-star).
|
|
181
|
+
- `unstar` toggles like off and keeps star level.
|
|
182
|
+
|
|
169
183
|
## Expose to LAN
|
|
170
184
|
|
|
171
185
|
Set:
|
|
@@ -184,6 +198,22 @@ If you run behind a reverse proxy, forward `X-Forwarded-Proto` and `X-Forwarded-
|
|
|
184
198
|
|
|
185
199
|
Without HTTPS, credentials travel unencrypted on your network.
|
|
186
200
|
|
|
201
|
+
## Plex Webhooks (Optional, Recommended)
|
|
202
|
+
|
|
203
|
+
Webhook purpose: Plex notifies Plexsonic on library/media events so Plexsonic can refresh caches faster and reduce stale results.
|
|
204
|
+
|
|
205
|
+
Plex does not auto-discover Plexsonic. You must add the webhook URL in Plex Media Server settings.
|
|
206
|
+
|
|
207
|
+
1. Open Plex Media Server settings, then `Network` -> `Webhooks`.
|
|
208
|
+
2. Add your Plexsonic endpoint URL.
|
|
209
|
+
Without token: `http://<your-host>:3127/webhooks/plex`
|
|
210
|
+
With token: `http://<your-host>:3127/webhooks/plex?token=<PLEX_WEBHOOK_TOKEN>`
|
|
211
|
+
3. Save settings in Plex.
|
|
212
|
+
|
|
213
|
+
Notes:
|
|
214
|
+
- `BASE_URL` is not required for webhook processing.
|
|
215
|
+
- Webhook URL must be reachable by your Plex Media Server (LAN IP/hostname or public URL, depending on your setup).
|
|
216
|
+
|
|
187
217
|
## Notes
|
|
188
218
|
|
|
189
219
|
- This project currently targets practical client compatibility over strict parity with any single server implementation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plexsonic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "PlexMusic to OpenSubsonic bridge",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@fastify/cookie": "^11.0.2",
|
|
23
23
|
"@fastify/formbody": "^8.0.2",
|
|
24
|
-
"@fastify/
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
24
|
+
"@fastify/multipart": "^9.4.0",
|
|
25
|
+
"@fastify/session": "^11.1.1",
|
|
26
|
+
"argon2": "^0.44.0",
|
|
27
|
+
"better-sqlite3": "^12.6.2",
|
|
28
|
+
"dotenv": "^17.2.4",
|
|
29
|
+
"fastify": "^5.7.4",
|
|
29
30
|
"pino-pretty": "^13.1.3"
|
|
30
31
|
},
|
|
31
32
|
"pnpm": {
|
package/src/config.js
CHANGED
|
@@ -89,6 +89,7 @@ export function loadConfig(env = process.env) {
|
|
|
89
89
|
plexProduct: env.PLEX_PRODUCT || DEFAULT_PLEX_PRODUCT,
|
|
90
90
|
plexClientIdentifier:
|
|
91
91
|
env.PLEX_CLIENT_IDENTIFIER || deriveClientIdentifier(path.resolve(sqlitePath)),
|
|
92
|
+
plexWebhookToken: env.PLEX_WEBHOOK_TOKEN || '',
|
|
92
93
|
licenseEmail: env.LICENSE_EMAIL || '',
|
|
93
94
|
logLevel: env.LOG_LEVEL || DEFAULT_LOG_LEVEL,
|
|
94
95
|
logRequests: parseBoolean(env.LOG_REQUESTS, DEFAULT_LOG_REQUESTS),
|
package/src/plex.js
CHANGED
|
@@ -199,10 +199,20 @@ function extractMetadataList(container) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function normalizeLibrarySection(section) {
|
|
202
|
+
const parseTimestamp = (value) => {
|
|
203
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
204
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
205
|
+
};
|
|
206
|
+
|
|
202
207
|
return {
|
|
203
208
|
id: String(section.key ?? section.id ?? ''),
|
|
204
209
|
title: section.title || section.name || 'Untitled',
|
|
205
210
|
type: section.type || '',
|
|
211
|
+
updatedAt: parseTimestamp(section.updatedAt),
|
|
212
|
+
scannedAt: parseTimestamp(section.scannedAt),
|
|
213
|
+
refreshedAt: parseTimestamp(section.refreshedAt),
|
|
214
|
+
contentChangedAt: parseTimestamp(section.contentChangedAt),
|
|
215
|
+
leafCount: parseTimestamp(section.leafCount),
|
|
206
216
|
};
|
|
207
217
|
}
|
|
208
218
|
|
|
@@ -804,6 +814,35 @@ export async function listTracks({ baseUrl, plexToken, sectionId }) {
|
|
|
804
814
|
return extractMetadataList(payload.MediaContainer);
|
|
805
815
|
}
|
|
806
816
|
|
|
817
|
+
export async function probeSectionFingerprint({ baseUrl, plexToken, sectionId, signal = undefined }) {
|
|
818
|
+
const payload = await fetchPmsJson(
|
|
819
|
+
baseUrl,
|
|
820
|
+
plexToken,
|
|
821
|
+
`/library/sections/${encodeURIComponent(sectionId)}/all`,
|
|
822
|
+
{
|
|
823
|
+
type: 10,
|
|
824
|
+
sort: 'updatedAt:desc',
|
|
825
|
+
'X-Plex-Container-Start': 0,
|
|
826
|
+
'X-Plex-Container-Size': 1,
|
|
827
|
+
},
|
|
828
|
+
{ signal },
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const container = payload?.MediaContainer || {};
|
|
832
|
+
const firstTrack = extractMetadataList(container)[0] || {};
|
|
833
|
+
const parseIntSafe = (value) => {
|
|
834
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
835
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const totalSize = parseIntSafe(container.totalSize);
|
|
839
|
+
const ratingKey = String(firstTrack.ratingKey || '');
|
|
840
|
+
const updatedAt = parseIntSafe(firstTrack.updatedAt);
|
|
841
|
+
const addedAt = parseIntSafe(firstTrack.addedAt);
|
|
842
|
+
|
|
843
|
+
return `${sectionId}|${totalSize}|${ratingKey}|${updatedAt}|${addedAt}`;
|
|
844
|
+
}
|
|
845
|
+
|
|
807
846
|
export async function getArtist({ baseUrl, plexToken, artistId }) {
|
|
808
847
|
const payload = await fetchPmsJson(baseUrl, plexToken, `/library/metadata/${encodeURIComponent(artistId)}`);
|
|
809
848
|
const item = extractMetadataList(payload.MediaContainer)[0] || null;
|