diff --git a/.dockerignore b/.dockerignore index 4137a21..d9a3e74 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ node_modules npm-debug.log .vscode -src +# src .env* \ No newline at end of file diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 0000000..cbc8ca1 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,61 @@ +# .github/workflows/publish-docker.yml + +name: Publish Docker Image + +# Run this workflow on every push to your main branch +on: + push: + branches: [ "master" ] # Change to "main" if that's your default branch + +jobs: + build-and-push: + # Use the latest version of Ubuntu to run the job + runs-on: ubuntu-latest + # Grant permissions for the job to write to the GitHub Package Registry + permissions: + contents: read + packages: write + + steps: + # 1. Checks out your repository's code + - name: Checkout repository + uses: actions/checkout@v4 + + # 2. Sets up QEMU for multi-platform build emulation + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # 3. Sets up Docker Buildx, the builder that can create multi-platform images + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # 4. Logs into the GitHub Container Registry using the secret you created + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + # 5. Extracts useful metadata like image tags + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + # tag with :latest for pushes to the default branch + type=raw,value=latest,enable={{is_default_branch}} + # tag with the branch name for other branches + type=ref,event=branch + # tag with the git sha + type=sha + # 6. Builds the image from your Dockerfile and pushes it to GHCR + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 244588b..e17fe3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,10 @@ RUN npm install # Bundle app source COPY . . +# --- NEW DEBUGGING STEP --- +# List the files and directories to confirm they were copied correctly. +RUN find . -maxdepth 2 + +RUN npm run build + CMD [ "node", "dist/index.js" ] \ No newline at end of file diff --git a/docker-compose-example.yml b/docker-compose-example.yml index 5daed5b..816ea95 100644 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -1,7 +1,7 @@ version: "2.1" services: plex-requester-collections: - image: manybothans/plex-requester-collections:latest + image: ghcr.io/ahmaddxb/plex-requester-collections:latest container_name: plex-requester-collections environment: - NODE_ENV=production # Optional - 'development' will give verbose logging. diff --git a/package-lock.json b/package-lock.json index 310b00f..2941bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plex-requester-collections", - "version": "1.0.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plex-requester-collections", - "version": "1.0.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "@types/lodash": "^4.14.194", @@ -14,7 +14,8 @@ "axios": "^1.4.0", "dotenv": "^16.0.3", "lodash": "^4.17.21", - "moment": "^2.29.4" + "moment": "^2.29.4", + "typescript": "^5.0.4" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.59.5", diff --git a/package.json b/package.json index 2005e6a..3f0941c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "dist/index.js", "scripts": { "start": "tsc && node dist/index.js", + "build": "tsc", "lint": "eslint . --ext .ts", "test": "echo \"Error: no test specified\" && exit 1", "dockerize": "tsc && docker build . -t manybothans/plex-requester-collections", diff --git a/src/index.ts b/src/index.ts index 99eb880..e32137a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ const COLL_TITLE_PREFIX_SHOW = "TV Shows Requested by "; const STALE_ADDED_DATE_THRESHOLD = moment().subtract(6, "months"); const STALE_VIEW_DATE_THRESHOLD = moment().subtract(3, "months"); const MS_24_HOURS = 86400000; +const TAG_PREFIX_WATCHED_BY = "watched_by:"; // Start after a delay, if set. const startDelay = @@ -325,6 +326,66 @@ const app = async function () { order_dir: "desc" }); + + // ========================================================================= + // === FEATURE: Watched By Tagging (Controlled by FEATURE_WATCHED_BY flag) + // ========================================================================= + if (process.env.FEATURE_WATCHED_BY === "1") { + console.log(" -> 'Watched By' feature is enabled."); + + /************************************************************************************ + * Clean up old watched_by tags before adding new ones. + ************************************************************************************/ + const allRadarrTags = await RadarrAPI.getTags(); + if (allRadarrTags && radarrSonarrItem.tags.length > 0) { + const radarrTagMap = new Map(allRadarrTags.map(tag => [tag.id, tag.label])); + const viewerTagsToRemove = radarrSonarrItem.tags + .map(tagId => radarrTagMap.get(tagId)) + .filter(tagName => tagName && tagName.startsWith(TAG_PREFIX_WATCHED_BY)); + + if (viewerTagsToRemove.length > 0) { + console.log(` -> Found ${viewerTagsToRemove.length} old watched_by tag(s) to remove.`); + for (const tagName of viewerTagsToRemove) { + console.log(` -> Removing old tag: ${tagName}`); + radarrSonarrItem = await RadarrAPI.removeTagFromMediaItem( + radarrSonarrItem.id, + tagName, + radarrSonarrItem + ) || radarrSonarrItem; + } + } + } + + /************************************************************************************ + * Add tags for all users who have watched the movie. + ************************************************************************************/ + const uniqueViewers = _.uniqBy( + _.filter( + histories, + (session: TautulliHistoryDetails) => session && session.user + ), + "user" + ) as TautulliHistoryDetails[]; + + if (uniqueViewers && uniqueViewers.length > 0) { + console.log(` -> Found ${uniqueViewers.length} current unique viewer(s).`); + for (const viewer of uniqueViewers) { + const nameForTag = viewer.friendly_name || viewer.user; + const viewerTag = TAG_PREFIX_WATCHED_BY + nameForTag; + console.log(` -> Applying Radarr tag for viewer: ${nameForTag}`); + radarrSonarrItem = await RadarrAPI.addTagToMediaItem( + radarrSonarrItem?.id, + viewerTag, + radarrSonarrItem + ) || radarrSonarrItem; + } + } + } + // ========================================================================= + // === END of FEATURE: Watched By Tagging + // ========================================================================= + + // Filter history sessions to look at everyone expect the requester. const filteredHistories_others = _.filter( histories,