├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build-api-binary.yml.old │ ├── docker-master.yml │ ├── docker-preview.yml │ └── docker.yml ├── .gitignore ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── admin ├── .DS_Store ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── public │ ├── config.json │ ├── favicon │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ ├── images │ │ ├── no-poster-person.jpg │ │ ├── no-poster.jpg │ │ └── splash │ │ │ ├── apple-splash-1125-2436.jpg │ │ │ ├── apple-splash-1170-2532.jpg │ │ │ ├── apple-splash-1242-2208.jpg │ │ │ ├── apple-splash-1242-2688.jpg │ │ │ ├── apple-splash-1284-2778.jpg │ │ │ ├── apple-splash-1536-2048.jpg │ │ │ ├── apple-splash-1620-2160.jpg │ │ │ ├── apple-splash-1668-2224.jpg │ │ │ ├── apple-splash-1668-2388.jpg │ │ │ ├── apple-splash-2048-2732.jpg │ │ │ ├── apple-splash-640-1136.jpg │ │ │ ├── apple-splash-750-1334.jpg │ │ │ └── apple-splash-828-1792.jpg │ ├── index.html │ ├── manifest.json │ ├── p-seamless.png │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── assets │ ├── fonts │ │ ├── Khula-Bold.eot │ │ ├── Khula-Bold.ttf │ │ ├── Khula-Bold.woff │ │ └── Khula-Bold.woff2 │ └── svg │ │ ├── 1080p.svg │ │ ├── 4k.svg │ │ ├── 720p.svg │ │ ├── admin.svg │ │ ├── arrow-left.svg │ │ ├── bookmark.svg │ │ ├── buffer.svg │ │ ├── check.svg │ │ ├── close.svg │ │ ├── console.svg │ │ ├── dashboard.svg │ │ ├── direct.svg │ │ ├── docker.svg │ │ ├── filter.svg │ │ ├── issue.svg │ │ ├── linux.svg │ │ ├── lock.svg │ │ ├── mac.svg │ │ ├── minus-circle.svg │ │ ├── movie.svg │ │ ├── notifications.svg │ │ ├── pause.svg │ │ ├── people.svg │ │ ├── person-circle.svg │ │ ├── play.svg │ │ ├── plus-circle.svg │ │ ├── report.svg │ │ ├── request.svg │ │ ├── search.svg │ │ ├── server.svg │ │ ├── settings-general.svg │ │ ├── settings.svg │ │ ├── spinner.svg │ │ ├── star.svg │ │ ├── stream.svg │ │ ├── tmdb.svg │ │ ├── transcode.svg │ │ ├── tv.svg │ │ ├── unlock.svg │ │ ├── unraid.svg │ │ ├── video.svg │ │ ├── warning.svg │ │ └── windows.svg │ ├── components │ ├── Bandwidth.js │ ├── Carousel.js │ ├── CarouselLoading.js │ ├── Cpu.js │ ├── FilterAction.js │ ├── FilterItem.js │ ├── FilterRow.js │ ├── Login.js │ ├── Modal.js │ ├── MovieCard.js │ ├── Ram.js │ ├── RequestCard.js │ ├── RequestsTable.js │ ├── SessionMedia.js │ ├── Sessions.js │ ├── Sidebar.js │ └── TvCard.js │ ├── data │ ├── Api │ │ ├── actions.js │ │ ├── api.js │ │ ├── index.js │ │ └── reducer.js │ ├── Languages │ │ └── languages.js │ ├── Plex │ │ ├── actions.js │ │ ├── api.js │ │ ├── index.js │ │ └── reducer.js │ ├── User │ │ ├── actions.js │ │ ├── api.js │ │ ├── index.js │ │ └── reducer.js │ ├── actionTypes.js │ ├── http.js │ ├── reducers.js │ └── store.js │ ├── index.css │ ├── index.js │ ├── page │ ├── Dashboard.js │ ├── Issues.js │ ├── Profile.js │ ├── Requests.js │ ├── Reviews.js │ ├── Settings.js │ ├── Setup.js │ ├── Users.js │ ├── settings │ │ ├── console.js │ │ ├── filter.js │ │ ├── general.js │ │ ├── notifications.js │ │ ├── radarr.js │ │ └── sonarr.js │ └── users │ │ └── Profiles.js │ ├── reportWebVitals.js │ ├── serviceWorker.js │ ├── setupTests.js │ └── styles │ ├── components │ ├── buttons.scss │ ├── card.scss │ ├── carousel.scss │ ├── image-upload.scss │ ├── input.scss │ ├── issues.scss │ ├── messages.scss │ ├── modal.scss │ ├── review.scss │ ├── search-form.scss │ ├── section.scss │ ├── sessions.scss │ ├── sonarr-radarr.scss │ ├── spinner.scss │ └── table.scss │ ├── globals │ ├── body.scss │ ├── margins.scss │ ├── page.scss │ ├── sidebar.scss │ └── type.scss │ ├── main.scss │ ├── pages │ ├── dashboard.scss │ ├── login.scss │ ├── profile.scss │ ├── requests.scss │ ├── settings.scss │ └── setup.scss │ └── pre │ ├── fonts.scss │ ├── mixins.scss │ └── normalize.scss ├── api ├── .DS_Store ├── .dockerignore ├── .editorconfig ├── .gitignore ├── app.js ├── discovery │ ├── build.js │ └── display.js ├── fanart │ └── index.js ├── mail │ ├── mailer.js │ └── template.html ├── meta │ ├── imdb.js │ └── musicBrainz.js ├── middleware │ └── auth.js ├── models │ ├── archive.js │ ├── artist.js │ ├── discovery.js │ ├── filter.js │ ├── imdb.js │ ├── issue.js │ ├── library.js │ ├── movie.js │ ├── profile.js │ ├── request.js │ ├── review.js │ ├── show.js │ └── user.js ├── notifications │ ├── discord.js │ └── telegram.js ├── package.json ├── plex │ ├── bandwidth.js │ ├── history.js │ ├── libraryUpdate.js │ ├── onServer.js │ ├── plexLookup.js │ ├── serverInfo.js │ ├── sessions.js │ ├── testConnection.js │ └── top.js ├── requests │ ├── archive.js │ ├── display.js │ ├── filter.js │ ├── process.js │ └── quotas.js ├── routes │ ├── batch.js │ ├── config.js │ ├── discovery.js │ ├── filter.js │ ├── genie.js │ ├── history.js │ ├── issue.js │ ├── log.js │ ├── login.js │ ├── mail.js │ ├── movie.js │ ├── notifications.js │ ├── person.js │ ├── plex.js │ ├── profiles.js │ ├── request.js │ ├── review.js │ ├── search.js │ ├── services.js │ ├── sessions.js │ ├── show.js │ ├── top.js │ ├── trending.js │ └── user.js ├── services │ ├── radarr.js │ ├── sonarr.js │ └── sonarrOld.js ├── tmdb │ ├── languages.js │ ├── movie.js │ ├── person.js │ ├── search.js │ ├── show.js │ └── trending.js ├── util │ ├── config.js │ ├── logger.js │ └── setupReady.js └── worker.js ├── docker-compose.yml ├── frontend ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── public │ ├── config.json │ ├── favicon │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ ├── fonts │ │ ├── Khula-Bold.eot │ │ ├── Khula-Bold.ttf │ │ ├── Khula-Bold.woff │ │ └── Khula-Bold.woff2 │ ├── images │ │ ├── no-poster-person.jpg │ │ ├── no-poster.jpg │ │ └── splash │ │ │ ├── apple-splash-1125-2436.jpg │ │ │ ├── apple-splash-1170-2532.jpg │ │ │ ├── apple-splash-1242-2208.jpg │ │ │ ├── apple-splash-1242-2688.jpg │ │ │ ├── apple-splash-1284-2778.jpg │ │ │ ├── apple-splash-1536-2048.jpg │ │ │ ├── apple-splash-1620-2160.jpg │ │ │ ├── apple-splash-1668-2224.jpg │ │ │ ├── apple-splash-1668-2388.jpg │ │ │ ├── apple-splash-2048-2732.jpg │ │ │ ├── apple-splash-640-1136.jpg │ │ │ ├── apple-splash-750-1334.jpg │ │ │ └── apple-splash-828-1792.jpg │ ├── index.html │ ├── manifest.json │ ├── p-seamless.png │ ├── petio_splash.jpg │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── assets │ ├── fonts │ │ ├── Khula-Bold.eot │ │ ├── Khula-Bold.ttf │ │ ├── Khula-Bold.woff │ │ └── Khula-Bold.woff2 │ └── svg │ │ ├── 1080p.svg │ │ ├── 4k.svg │ │ ├── 720p.svg │ │ ├── admin.svg │ │ ├── back.svg │ │ ├── bookmark.svg │ │ ├── check.svg │ │ ├── close.svg │ │ ├── forward.svg │ │ ├── genres │ │ ├── action.svg │ │ ├── adventure.svg │ │ ├── animation.svg │ │ ├── anime.svg │ │ ├── comedy.svg │ │ ├── crime.svg │ │ ├── documentary.svg │ │ ├── drama.svg │ │ ├── family.svg │ │ ├── fantasy.svg │ │ ├── history.svg │ │ ├── horror.svg │ │ ├── music.svg │ │ ├── mystery.svg │ │ ├── romance.svg │ │ ├── science-fiction.svg │ │ ├── thriller.svg │ │ ├── tv-movie.svg │ │ ├── war.svg │ │ └── western.svg │ │ ├── imdb.svg │ │ ├── movie.svg │ │ ├── people.svg │ │ ├── person-circle.svg │ │ ├── play.svg │ │ ├── report.svg │ │ ├── request.svg │ │ ├── rt-fresh.svg │ │ ├── rt-none.svg │ │ ├── rt-rotten.svg │ │ ├── search.svg │ │ ├── server.svg │ │ ├── settings.svg │ │ ├── spinner.svg │ │ ├── star.svg │ │ ├── tmdb-sm.svg │ │ ├── tmdb.svg │ │ ├── tv.svg │ │ ├── video.svg │ │ └── warning.svg │ ├── components │ ├── Carousel.js │ ├── CarouselLoading.js │ ├── CarouselLoadingCompany.js │ ├── CarouselLoadingPerson.js │ ├── CompanyCard.js │ ├── History.js │ ├── Issues.js │ ├── MovieCard.js │ ├── MovieShowLoading.js │ ├── MovieShowOverview.js │ ├── MovieShowTop.js │ ├── MyRequests.js │ ├── PersonCard.js │ ├── Popular.js │ ├── RequestCard.js │ ├── Review.js │ ├── ReviewsLists.js │ ├── Sidebar.js │ └── TvCard.js │ ├── data │ ├── Api │ │ ├── actions.js │ │ ├── api.js │ │ ├── index.js │ │ └── reducer.js │ ├── Nav │ │ ├── index.js │ │ └── reducer.js │ ├── Plex │ │ └── api.js │ ├── User │ │ ├── actions.js │ │ ├── api.js │ │ ├── index.js │ │ └── reducer.js │ ├── actionTypes.js │ ├── auth │ │ └── index.js │ ├── http.js │ ├── reducers.js │ └── store.js │ ├── index.js │ ├── pages │ ├── Actor.js │ ├── Company.js │ ├── Genre.js │ ├── Movie.js │ ├── Movies.js │ ├── Networks.js │ ├── People.js │ ├── Profile.js │ ├── Requests.js │ ├── Search.js │ ├── Season.js │ ├── Series.js │ └── Shows.js │ ├── serviceWorker.js │ ├── setupTests.js │ └── styles │ ├── components │ ├── buttons.scss │ ├── calendar.scss │ ├── card.scss │ ├── carousel.scss │ ├── config-setup.scss │ ├── input.scss │ ├── issues.scss │ ├── messages.scss │ ├── my-requests.scss │ ├── push-msg.scss │ ├── review.scss │ ├── search-form.scss │ ├── section.scss │ ├── spinner.scss │ └── table.scss │ ├── globals │ ├── body.scss │ ├── margins.scss │ ├── page.scss │ ├── sidebar.scss │ └── type.scss │ ├── main.scss │ ├── pages │ ├── companies.scss │ ├── genre.scss │ ├── login.scss │ ├── media.scss │ ├── networks.scss │ ├── person.scss │ ├── profile.scss │ └── season.scss │ └── pre │ ├── fonts.scss │ ├── mixins.scss │ └── normalize.scss ├── package.json ├── petio.js └── router.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | api/node_modules 3 | api/config.json 4 | admin/node_modules 5 | frontend/node_modules 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | insert_final_newline = false 11 | trim_trailing_whitespace = false 12 | 13 | [*.{js,jsx,json,ts,tsx,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [Makefile] 18 | indent_size = 1 19 | indent_style = tab 20 | 21 | [COMMIT_EDITMSG] 22 | max_line_length = 0 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | target-branch: "dev" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "npm" 9 | directory: "/admin" 10 | target-branch: "dev" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "npm" 14 | directory: "/api" 15 | target-branch: "dev" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: "npm" 19 | directory: "/frontend" 20 | target-branch: "dev" 21 | schedule: 22 | interval: "daily" 23 | - package-ecosystem: "docker" 24 | directory: "/" 25 | target-branch: "dev" 26 | schedule: 27 | interval: "daily" 28 | - package-ecosystem: "docker" 29 | directory: "/api" 30 | target-branch: "dev" 31 | schedule: 32 | interval: "daily" 33 | -------------------------------------------------------------------------------- /.github/workflows/build-api-binary.yml.old: -------------------------------------------------------------------------------- 1 | name: Binary - API 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | api-binary: 10 | name: Build Binary 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | - name: Install packages 28 | run: npm ci 29 | working-directory: api 30 | - name: Run build package 31 | run: npm run build 32 | working-directory: api 33 | - uses: actions/upload-artifact@v2 34 | with: 35 | name: api 36 | path: api/bin/ -------------------------------------------------------------------------------- /.github/workflows/docker-master.yml: -------------------------------------------------------------------------------- 1 | name: Docker Master Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | docker-aio: 9 | name: Docker Master Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build Docker image 14 | run: docker build -t ghcr.io/${{ github.repository }}:latest -t ghcr.io/${{ github.repository }}:latest-${GITHUB_SHA::8} . 15 | - name: Docker login 16 | run: echo ${{ secrets.GHCR_TOKEN }} | docker login -u ${{ github.repository_owner }} --password-stdin ghcr.io 17 | - name: Push docker image 18 | run: docker push ghcr.io/${{ github.repository }}:latest 19 | -------------------------------------------------------------------------------- /.github/workflows/docker-preview.yml: -------------------------------------------------------------------------------- 1 | name: Docker Preview Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - preview 7 | jobs: 8 | docker-aio: 9 | name: Docker Preview Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build Docker image 14 | run: docker build -t ghcr.io/${{ github.repository }}:preview -t ghcr.io/${{ github.repository }}:preview-${GITHUB_SHA::8} . 15 | - name: Docker login 16 | run: echo ${{ secrets.GHCR_TOKEN }} | docker login -u ${{ github.repository_owner }} --password-stdin ghcr.io 17 | - name: Push docker image 18 | run: docker push ghcr.io/${{ github.repository }}:preview 19 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Dev Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | jobs: 8 | docker-aio: 9 | name: Docker Dev Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build Docker image 14 | run: docker build -t ghcr.io/${{ github.repository }}:dev -t ghcr.io/${{ github.repository }}:dev-${GITHUB_SHA::8} . 15 | - name: Docker login 16 | run: echo ${{ secrets.GHCR_TOKEN }} | docker login -u ${{ github.repository_owner }} --password-stdin ghcr.io 17 | - name: Push docker image 18 | run: docker push ghcr.io/${{ github.repository }}:dev 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | api/node_modules/ 3 | admin/node_modules/ 4 | frontend/node_modules/ 5 | node_modules/ 6 | .DS_Store 7 | api/config.json 8 | api/.DS_Store 9 | frontend/build 10 | admin/build 11 | api/config/ 12 | api/config-backup/ 13 | api/logs/ 14 | bin/ 15 | *.zip 16 | views/ 17 | frontend/.eslintcache 18 | admin/.eslintcache 19 | /logs 20 | api/.DS_Store 21 | api/imdb_dump.txt 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.3.0-alpine3.13 as builder 2 | 3 | RUN apk add --no-cache git 4 | COPY ./ /source/ 5 | 6 | WORKDIR /build 7 | RUN cp /source/petio.js . && \ 8 | cp /source/router.js . && \ 9 | cp /source/package.json . && \ 10 | npm install && \ 11 | cp -R /source/frontend . && \ 12 | cp -R /source/admin . && \ 13 | cp -R /source/api . 14 | 15 | WORKDIR /build/frontend 16 | RUN npm install && \ 17 | npm run build 18 | 19 | WORKDIR /build/admin 20 | RUN npm install --legacy-peer-deps && \ 21 | npm run build 22 | 23 | WORKDIR /build/api 24 | RUN npm install --legacy-peer-deps 25 | 26 | WORKDIR /build/views 27 | RUN mv /build/frontend/build /build/views/frontend && \ 28 | rm -rf /build/frontend && \ 29 | mv /build/admin/build /build/views/admin && \ 30 | rm -rf /build/admin && \ 31 | chmod -R u=rwX,go=rX /build 32 | 33 | FROM alpine:3.13 34 | 35 | EXPOSE 7777 36 | VOLUME ["/app/api/config", "/app/logs"] 37 | WORKDIR /app 38 | ENTRYPOINT ["/sbin/tini", "--"] 39 | CMD [ "node", "petio.js" ] 40 | 41 | RUN apk add --no-cache nodejs tzdata tini 42 | 43 | COPY --from=builder /build/ /app/ 44 | 45 | LABEL org.opencontainers.image.vendor="petio-team" 46 | LABEL org.opencontainers.image.url="https://github.com/petio-team/petio" 47 | LABEL org.opencontainers.image.documentation="https://docs.petio.tv/" 48 | LABEL org.opencontainers.image.licenses="MIT" 49 | 50 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "wget", "--spider", "http://localhost:7777/health" ] 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Petio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker: 2 | docker-compose pull && docker-compose up -d 3 | 4 | pkg : 5 | rm -rf ./bin && mkdir ./bin 6 | cd api && npm install 7 | npm install && pkg petio.js --out-path "./bin" 8 | mkdir ./bin/views 9 | cd frontend && npm install && REACT_APP_ENV=pkg npm run build && mv build ../bin/views/frontend 10 | cd admin && npm install && REACT_APP_ENV=pkg npm run build && mv build ../bin/views/admin 11 | zip -r petio.zip ./bin 12 | rm -rf ./bin 13 | npm run stamp-version 14 | 15 | clean: 16 | rm -rf ./bin 17 | rm -rf node_modules && rm -rf admin/node_modules && rm -rf frontend/node_modules && rm -rf api/node_modules 18 | rm -rf admin/build && rm -rf frontend/build 19 | 20 | run: 21 | cd api && npm install 22 | rm -rf ./views && mkdir ./views 23 | cd frontend && npm install && REACT_APP_ENV=pkg npm run build && mv build ../views/frontend 24 | cd admin && npm install && REACT_APP_ENV=pkg npm run build && mv build ../views/admin 25 | npm install 26 | 27 | 28 | docker-stop: 29 | docker-compose down 30 | -------------------------------------------------------------------------------- /admin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/.DS_Store -------------------------------------------------------------------------------- /admin/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{package,bower}.json] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [{.eslintrc,.scss-lint.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.{scss,sass}] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /admin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["react", "prettier"], 15 | "rules": { 16 | "react/prop-types": 0, 17 | "no-class-assign": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | .eslintcache 4 | package-lock.json 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # Petio Admin Dashboard 2 | 3 | This is the react front end control panel for the Petio system. Features the initial setup wizard and management of Petio features. 4 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petio-admin", 3 | "version": "0.5.7-alpha", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "react": "^17.0.2", 11 | "react-charts": "^2.0.0-beta.7", 12 | "react-dom": "^17.0.2", 13 | "react-lazy-load-image-component": "^1.5.1", 14 | "react-redux": "^7.2.4", 15 | "react-router": "^5.1.2", 16 | "react-router-dom": "^5.2.0", 17 | "react-scripts": "4.0.2", 18 | "recharts": "^2.0.9", 19 | "redux": "^4.1.0", 20 | "redux-devtools-extension": "^2.13.9", 21 | "redux-thunk": "^2.3.0", 22 | "sass": "1.34.1", 23 | "uuid": "^8.3.2", 24 | "web-vitals": "^2.0.1" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | "since 2010" 41 | ], 42 | "development": [ 43 | "since 2010" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "eslint": "^7.29.0", 48 | "eslint-config-prettier": "^8.3.0", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "eslint-plugin-react": "^7.24.0", 51 | "prettier": "^2.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /admin/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "PlexRequestApi": "", 3 | "PlexRequestApiPort": "7778" 4 | } 5 | -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /admin/public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /admin/public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /admin/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /admin/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /admin/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /admin/public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /admin/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/favicon.ico -------------------------------------------------------------------------------- /admin/public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /admin/public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /admin/public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /admin/public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /admin/public/images/no-poster-person.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/no-poster-person.jpg -------------------------------------------------------------------------------- /admin/public/images/no-poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/no-poster.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /admin/public/images/splash/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/images/splash/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /admin/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Petio", 3 | "name": "Petio Plex Request", 4 | "icons": [ 5 | { 6 | "src": "/favicon/favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "/favicon/favicon-16x16.png", 12 | "type": "image/png", 13 | "sizes": "16x16" 14 | }, 15 | { 16 | "src": "/favicon/favicon-32x32.png", 17 | "type": "image/png", 18 | "sizes": "32x32" 19 | }, 20 | { 21 | "src": "/favicon/favicon-96x96.png", 22 | "type": "image/png", 23 | "sizes": "96x96" 24 | } 25 | ], 26 | "start_url": ".", 27 | "display": "standalone", 28 | "theme_color": "#000000", 29 | "background_color": "#ffffff" 30 | } 31 | -------------------------------------------------------------------------------- /admin/public/p-seamless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/public/p-seamless.png -------------------------------------------------------------------------------- /admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /admin/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /admin/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /admin/src/assets/fonts/Khula-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/src/assets/fonts/Khula-Bold.eot -------------------------------------------------------------------------------- /admin/src/assets/fonts/Khula-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/src/assets/fonts/Khula-Bold.ttf -------------------------------------------------------------------------------- /admin/src/assets/fonts/Khula-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/src/assets/fonts/Khula-Bold.woff -------------------------------------------------------------------------------- /admin/src/assets/fonts/Khula-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/admin/src/assets/fonts/Khula-Bold.woff2 -------------------------------------------------------------------------------- /admin/src/assets/svg/720p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /admin/src/assets/svg/admin.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/bookmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/buffer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/console.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/direct.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/docker.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/issue.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/mac.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/minus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/movie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/notifications.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/person-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/plus-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/report.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/request.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/settings-general.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /admin/src/assets/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/stream.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/transcode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/tv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/unlock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/unraid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /admin/src/assets/svg/video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/assets/svg/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /admin/src/assets/svg/windows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /admin/src/components/Carousel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Carousel extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
{this.props.children}
8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Carousel; 14 | -------------------------------------------------------------------------------- /admin/src/components/CarouselLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class CarouselLoading extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 |
9 |
10 |

11 | 12 |

13 |
14 |
15 |
16 |
17 |
18 |

19 | 20 |

21 |
22 |
23 |
24 |
25 |
26 |

27 | 28 |

29 |
30 |
31 |
32 |
33 |
34 |

35 | 36 |

37 |
38 |
39 |
40 |
41 |
42 |

43 | 44 |

45 |
46 |
47 |
48 |
49 |
50 |

51 | 52 |

53 |
54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | export default CarouselLoading; 61 | -------------------------------------------------------------------------------- /admin/src/components/Cpu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | LineChart, 4 | Line, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | Legend, 10 | ResponsiveContainer, 11 | } from 'recharts'; 12 | 13 | class Cpu extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | render() { 19 | let height = window.innerWidth >= 992 ? 300 : 200; 20 | let margin = 21 | window.innerWidth >= 992 22 | ? { top: 10, right: 0, left: -40, bottom: 0 } 23 | : { top: 10, right: 0, left: -60, bottom: 0 }; 24 | const formatter = (value) => value; 25 | return ( 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 48 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default Cpu; 64 | -------------------------------------------------------------------------------- /admin/src/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Modal extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 |
9 |

{this.props.title}

10 |
11 |
12 |
{this.props.children}
13 |
14 | {this.props.submit ? ( 15 |
16 | {this.props.submitText ? this.props.submitText : "Submit"} 17 |
18 | ) : ( 19 |
{this.props.submitText ? this.props.submitText : "Submit"}
20 | )} 21 |
22 | Cancel 23 |
24 | {this.props.delete ? ( 25 |
26 | {this.props.deleteText ? this.props.deleteText : "Delete"} 27 |
28 | ) : null} 29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default Modal; 38 | -------------------------------------------------------------------------------- /admin/src/components/Ram.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | LineChart, 4 | Line, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | Legend, 10 | ResponsiveContainer, 11 | } from 'recharts'; 12 | 13 | class Ram extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | render() { 19 | let height = window.innerWidth >= 992 ? 300 : 200; 20 | let margin = 21 | window.innerWidth >= 992 22 | ? { top: 10, right: 0, left: -40, bottom: 0 } 23 | : { top: 10, right: 0, left: -60, bottom: 0 }; 24 | const formatter = (value) => value; 25 | return ( 26 | 27 | 28 | 29 | = 992 ? true : false} 35 | /> 36 | 37 | 38 | 39 | 49 | 58 | 59 | 60 | ); 61 | } 62 | } 63 | 64 | export default Ram; 65 | -------------------------------------------------------------------------------- /admin/src/data/Api/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPopular, 3 | movie, 4 | series, 5 | search, 6 | clearSearch, 7 | person, 8 | top, 9 | history, 10 | get_plex_media, 11 | bandwidth, 12 | serverInfo, 13 | currentSessions, 14 | checkConfig, 15 | saveConfig, 16 | updateConfig, 17 | sonarrConfig, 18 | sonarrOptions, 19 | saveSonarrConfig, 20 | saveRadarrConfig, 21 | radarrConfig, 22 | radarrOptions, 23 | testSonarr, 24 | testRadarr, 25 | saveEmailConfig, 26 | getEmailConfig, 27 | testEmail, 28 | getConfig, 29 | getUser, 30 | allUsers, 31 | testMongo, 32 | getIssues, 33 | createUser, 34 | getProfiles, 35 | saveProfile, 36 | deleteProfile, 37 | editUser, 38 | bulkEditUser, 39 | deleteUser, 40 | removeRequest, 41 | updateRequest, 42 | getConsole, 43 | getReviews, 44 | removeIssue, 45 | updateFilters, 46 | getFilters, 47 | uploadThumb, 48 | testDiscord, 49 | testTelegram, 50 | testPlex, 51 | } from "./actions"; 52 | 53 | export default { 54 | getPopular, 55 | movie, 56 | series, 57 | search, 58 | clearSearch, 59 | person, 60 | top, 61 | history, 62 | get_plex_media, 63 | bandwidth, 64 | serverInfo, 65 | currentSessions, 66 | checkConfig, 67 | saveConfig, 68 | updateConfig, 69 | sonarrConfig, 70 | sonarrOptions, 71 | saveSonarrConfig, 72 | saveRadarrConfig, 73 | radarrConfig, 74 | radarrOptions, 75 | testSonarr, 76 | testRadarr, 77 | saveEmailConfig, 78 | getEmailConfig, 79 | testEmail, 80 | getConfig, 81 | getUser, 82 | allUsers, 83 | testMongo, 84 | getIssues, 85 | createUser, 86 | getProfiles, 87 | saveProfile, 88 | deleteProfile, 89 | editUser, 90 | bulkEditUser, 91 | deleteUser, 92 | removeRequest, 93 | updateRequest, 94 | getConsole, 95 | getReviews, 96 | removeIssue, 97 | updateFilters, 98 | getFilters, 99 | uploadThumb, 100 | testDiscord, 101 | testTelegram, 102 | testPlex, 103 | }; 104 | -------------------------------------------------------------------------------- /admin/src/data/Plex/api.js: -------------------------------------------------------------------------------- 1 | import { post } from "../http"; 2 | 3 | const plexHeaders = { 4 | "Content-Type": "application/json", 5 | Accept: "application/json", 6 | "X-Plex-Device": "API", 7 | "X-Plex-Device-Name": "Petio", 8 | "X-Plex-Product": "Petio", 9 | "X-Plex-Version": "v1.0", 10 | "X-Plex-Platform-Version": "v1.0", 11 | "X-Plex-Client-Identifier": "fc684eb1-cdff-46cc-a807-a3720696ae9f", 12 | }; 13 | 14 | export function getPins() { 15 | let url = "https://plex.tv/api/v2/pins?strong=true"; 16 | let method = "post"; 17 | let headers = plexHeaders; 18 | return process(url, headers, method).then((response) => response.json()); 19 | } 20 | 21 | export function validatePin(id) { 22 | let url = `https://plex.tv/api/v2/pins/${id}`; 23 | let method = "get"; 24 | let headers = plexHeaders; 25 | return process(url, headers, method).then((response) => response.json()); 26 | } 27 | 28 | export function getUser(token) { 29 | let url = `https://plex.tv/users/account?X-Plex-Token=${token}`; 30 | let method = "get"; 31 | let headers = plexHeaders; 32 | return process(url, headers, method) 33 | .then((response) => response.text()) 34 | .then((str) => new window.DOMParser().parseFromString(str, "text/xml")); 35 | } 36 | 37 | export function getServers(token, ssl = false) { 38 | let url = `https://plex.tv/pms/resources?${ 39 | ssl ? "includeHttps=1&" : "" 40 | }X-Plex-Token=${token}`; 41 | let method = "get"; 42 | let headers = plexHeaders; 43 | return process(url, headers, method) 44 | .then((response) => response.text()) 45 | .then((str) => new window.DOMParser().parseFromString(str, "text/xml")); 46 | } 47 | 48 | export async function plexLogin(token = false) { 49 | return post("/login/plex_login", { token: token }); 50 | } 51 | 52 | function process(url, headers, method, body = null) { 53 | let args = { 54 | method: method, 55 | headers: headers, 56 | }; 57 | 58 | if (method === "post") { 59 | args.body = body; 60 | } 61 | 62 | return fetch(url, args); 63 | } 64 | -------------------------------------------------------------------------------- /admin/src/data/Plex/index.js: -------------------------------------------------------------------------------- 1 | import { plexAuth, plexToken, plexLogin } from "./actions"; 2 | 3 | export default { 4 | plexAuth, 5 | plexToken, 6 | plexLogin, 7 | }; 8 | -------------------------------------------------------------------------------- /admin/src/data/Plex/reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes"; 2 | 3 | export default function ( 4 | state = { 5 | token: false, 6 | }, 7 | action 8 | ) { 9 | switch (action.type) { 10 | case types.PLEX_TOKEN: 11 | return { 12 | ...state, 13 | token: action.token, 14 | }; 15 | case types.PLEX_DETAILS: 16 | return { 17 | ...state, 18 | servers: action.servers, 19 | user: action.user, 20 | }; 21 | 22 | case types.PLEX_SERVER: 23 | return { 24 | ...state, 25 | servers: { 26 | ...state.servers, 27 | [action.key]: action.server, 28 | }, 29 | }; 30 | 31 | default: 32 | return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /admin/src/data/User/api.js: -------------------------------------------------------------------------------- 1 | import { get, post } from "../http"; 2 | 3 | export async function login(username, password, admin = false, token = false) { 4 | return post(`/login`, { 5 | user: { 6 | username: username, 7 | password: password, 8 | }, 9 | admin: admin, 10 | authToken: token, 11 | }); 12 | } 13 | 14 | export async function getRequests(min) { 15 | return get(`/request/${min ? "min" : "all"}`); 16 | } 17 | -------------------------------------------------------------------------------- /admin/src/data/User/index.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getRequests } from './actions'; 2 | 3 | export default { 4 | login, 5 | logout, 6 | getRequests, 7 | }; 8 | -------------------------------------------------------------------------------- /admin/src/data/User/reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actionTypes'; 2 | 3 | export default function ( 4 | state = { 5 | current: false, 6 | logged_in: false, 7 | library_index: false, 8 | email: false, 9 | requests: false, 10 | }, 11 | action 12 | ) { 13 | switch (action.type) { 14 | case types.LOGIN: 15 | return { 16 | ...state, 17 | current: action.data.user, 18 | logged_in: true, 19 | }; 20 | 21 | case types.LOGOUT: 22 | return { 23 | ...state, 24 | current: false, 25 | logged_in: false, 26 | credentials: false, 27 | }; 28 | 29 | case types.GET_REQUESTS: 30 | return { 31 | ...state, 32 | requests: action.requests, 33 | }; 34 | 35 | case types.GET_REVIEWS: 36 | return { 37 | ...state, 38 | reviews: { 39 | ...state.reviews, 40 | [action.id]: action.reviews, 41 | }, 42 | }; 43 | 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /admin/src/data/actionTypes.js: -------------------------------------------------------------------------------- 1 | // User 2 | export const LOGIN = "LOGIN"; 3 | export const LOGOUT = "LOGOUT"; 4 | 5 | export const GET_REVIEWS = "GET_REVIEWS"; 6 | export const GET_REQUESTS = "GET_REQUESTS"; 7 | export const PLEX_TOKEN = "PLEX_TOKEN"; 8 | export const PLEX_DETAILS = "PLEX_DETAILS"; 9 | export const PLEX_SERVER = "PLEX_SERVER"; 10 | export const GET_USER = "GET_USER"; 11 | export const ALL_USERS = "ALL_USERS"; 12 | 13 | // Metadata 14 | export const POPULAR = "POPULAR"; 15 | export const SEARCH = "SEARCH"; 16 | export const MOVIE_LOOKUP = "MOVIE_LOOKUP"; 17 | export const SERIES_LOOKUP = "SERIES_LOOKUP"; 18 | export const PERSON_LOOKUP = "PERSON_LOOKUP"; 19 | export const SEASON_LOOKUP = "SEASON_LOOKUP"; 20 | export const STORE_ACTOR_MOVIE = "STORE_ACTOR_MOVIE"; 21 | export const STORE_ACTOR_SERIES = "STORE_ACTOR_SERIES"; 22 | 23 | export const EXAMPLE = "EXAMPLE"; 24 | -------------------------------------------------------------------------------- /admin/src/data/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import api from './Api/reducer'; 3 | import plex from './Plex/reducer'; 4 | import user from './User/reducer'; 5 | 6 | const rootReducer = combineReducers({ 7 | plex, 8 | api, 9 | user, 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /admin/src/data/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import rootReducer from './reducers'; 3 | import thunk from 'redux-thunk'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | 6 | var store; 7 | 8 | function initStore(initialState) { 9 | store = createStore( 10 | rootReducer, 11 | composeWithDevTools(), 12 | initialState, 13 | applyMiddleware(thunk) 14 | ); 15 | } 16 | 17 | export { store, initStore }; 18 | -------------------------------------------------------------------------------- /admin/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | import { initStore, store } from "./data/store"; 6 | import { Provider } from "react-redux"; 7 | import "./styles/main.scss"; 8 | 9 | const startApp = () => { 10 | initStore(); 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById("root") 16 | ); 17 | }; 18 | 19 | if (!window.cordova) { 20 | startApp(); 21 | } else { 22 | document.addEventListener("deviceready", startApp, false); 23 | } 24 | serviceWorker.unregister(); 25 | -------------------------------------------------------------------------------- /admin/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /admin/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /admin/src/styles/components/image-upload.scss: -------------------------------------------------------------------------------- 1 | .image-upload { 2 | &--wrap { 3 | padding: 10px; 4 | background: rgba($grey-light, 0.1); 5 | border-radius: 5px; 6 | } 7 | 8 | &--inner { 9 | display: flex; 10 | align-items: center; 11 | margin-bottom: 10px; 12 | } 13 | 14 | &--current { 15 | min-width: 50px; 16 | min-height: 50px; 17 | border-radius: 50px; 18 | background: $dark-grey; 19 | margin-right: 20px; 20 | background-position: center; 21 | background-size: cover; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /admin/src/styles/components/issues.scss: -------------------------------------------------------------------------------- 1 | .issue-sidebar { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | z-index: 1000; 6 | background: $black; 7 | box-shadow: 0 0 0 rgba(black, 0.4); 8 | height: 100vh; 9 | max-height: 100vh; 10 | width: 90%; 11 | padding: 20px; 12 | transform: translateX(100%); 13 | transition: all 0.4s ease; 14 | overflow-y: auto; 15 | 16 | @include media-breakpoint-up(md) { 17 | width: 500px; 18 | padding: 50px; 19 | } 20 | 21 | &.open { 22 | box-shadow: -10px 0 20px rgba(black, 0.4); 23 | transform: translateX(0); 24 | } 25 | 26 | &--close { 27 | appearance: none; 28 | border: none; 29 | position: absolute; 30 | top: 0; 31 | right: 0; 32 | width: 50px; 33 | height: 50px; 34 | background: $primary; 35 | cursor: pointer; 36 | 37 | &:after, 38 | &:before { 39 | content: ""; 40 | width: 20px; 41 | height: 2px; 42 | background: white; 43 | position: absolute; 44 | top: 50%; 45 | left: 50%; 46 | } 47 | 48 | &:before { 49 | transform: translate(-50%, -50%) rotate(45deg); 50 | } 51 | 52 | &:after { 53 | transform: translate(-50%, -50%) rotate(-45deg); 54 | } 55 | } 56 | 57 | input { 58 | width: 100%; 59 | outline: none !important; 60 | border: none; 61 | border-radius: 0 !important; 62 | border-bottom: solid 2px $grey-medium; 63 | background: white; 64 | font-size: 16px; 65 | padding: 10px; 66 | margin-bottom: 10px; 67 | 68 | &:focus { 69 | border-bottom: solid 2px $primary; 70 | } 71 | } 72 | 73 | select { 74 | appearance: none; 75 | 76 | @extend input; 77 | } 78 | 79 | textarea { 80 | @extend input; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /admin/src/styles/components/messages.scss: -------------------------------------------------------------------------------- 1 | .push-msg { 2 | &--wrap { 3 | position: fixed; 4 | bottom: 60px; 5 | right: 10px; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: flex-end; 9 | z-index: 1000; 10 | 11 | @include media-breakpoint-up(lg) { 12 | bottom: 10px; 13 | right: 10px; 14 | } 15 | } 16 | 17 | &--item { 18 | width: 200px; 19 | padding: 10px 20px; 20 | background: $dark-grey; 21 | border-radius: 5px; 22 | margin-top: 5px; 23 | border-left: solid 5px $blue; 24 | 25 | @include media-breakpoint-up(lg) { 26 | width: 300px; 27 | padding: 20px 20px; 28 | margin-top: 10px; 29 | } 30 | 31 | &.good { 32 | border-color: $good; 33 | } 34 | 35 | &.error { 36 | border-color: $bad; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /admin/src/styles/components/modal.scss: -------------------------------------------------------------------------------- 1 | // 2 | 3 | .modal { 4 | &--wrap { 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: 999; 9 | width: 100%; 10 | height: 100vh; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | opacity: 0; 15 | pointer-events: none; 16 | transition: all 0.3s ease; 17 | 18 | @include media-breakpoint-up(lg) { 19 | background: rgba(0, 0, 0, 0.6); 20 | } 21 | 22 | &.active { 23 | opacity: 1; 24 | pointer-events: all; 25 | } 26 | } 27 | 28 | &--inner { 29 | width: 500px; 30 | max-width: calc(100% - 40px); 31 | background: $black; 32 | border-radius: 5px; 33 | overflow: hidden; 34 | overflow-y: auto; 35 | max-height: calc(100vh - 40px); 36 | } 37 | 38 | &--top { 39 | background: $primary; 40 | padding: 10px 20px; 41 | 42 | h3 { 43 | font-size: 15px; 44 | text-transform: uppercase; 45 | letter-spacing: 1px; 46 | } 47 | } 48 | 49 | &--main { 50 | padding: 20px; 51 | 52 | section { 53 | padding-left: 0 !important; 54 | padding-right: 0 !important; 55 | } 56 | 57 | select, 58 | .select-wrap, 59 | input:not([type="checkbox"]), 60 | textarea { 61 | width: 100%; 62 | } 63 | 64 | .btn { 65 | margin-right: 10px; 66 | } 67 | 68 | label { 69 | display: flex; 70 | align-items: center; 71 | margin-bottom: 20px; 72 | 73 | input { 74 | margin-right: 15px; 75 | } 76 | } 77 | 78 | .modal-btns { 79 | display: flex; 80 | 81 | .delete-modal { 82 | margin-left: auto; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /admin/src/styles/components/search-form.scss: -------------------------------------------------------------------------------- 1 | .search-form { 2 | display: flex; 3 | align-items: center; 4 | width: 400px; 5 | max-width: 100%; 6 | border-radius: 50px; 7 | overflow: hidden; 8 | 9 | input { 10 | height: 40px; 11 | border: none; 12 | background: rgba(255, 255, 255, 0.1); 13 | padding: 0 0 0 20px; 14 | font-size: 16px; 15 | border-radius: 0 !important; 16 | border: none; 17 | outline: none !important; 18 | color: white; 19 | width: 100%; 20 | 21 | &::placeholder { 22 | color: rgba(255, 255, 255, 0.2); 23 | } 24 | } 25 | 26 | .search-btn { 27 | width: 50px; 28 | height: 40px; 29 | background: rgba(255, 255, 255, 0.1); 30 | border: none; 31 | outline: none !important; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | 36 | svg { 37 | height: 20px; 38 | width: auto; 39 | 40 | path { 41 | fill: rgba(255, 255, 255, 0.2); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /admin/src/styles/components/section.scss: -------------------------------------------------------------------------------- 1 | section { 2 | padding-bottom: 25px; 3 | border-bottom: solid 1px rgba(white, 0.1); 4 | margin-bottom: 25px; 5 | 6 | @include media-breakpoint-down(md) { 7 | overflow-x: auto; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /admin/src/styles/components/spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | svg { 12 | width: 50px; 13 | height: 50px; 14 | } 15 | 16 | &--settings { 17 | position: fixed; 18 | top: 0; 19 | left: 500px; 20 | width: calc(100% - 500px); 21 | height: 100%; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | pointer-events: none; 26 | 27 | svg { 28 | width: 50px; 29 | height: 50px; 30 | } 31 | } 32 | 33 | &--requests { 34 | position: fixed; 35 | top: 0; 36 | left: 250px; 37 | width: calc(100% - 250px); 38 | height: 100%; 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | pointer-events: none; 43 | 44 | svg { 45 | width: 50px; 46 | height: 50px; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /admin/src/styles/globals/body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | // background: red; 3 | max-width: 100vw; 4 | overflow: hidden; 5 | color: $black; 6 | background: linear-gradient( 7 | 135deg, 8 | rgba(51, 59, 58, 1) 0%, 9 | rgba(55, 65, 65, 1) 25%, 10 | rgba(64, 54, 43, 1) 75%, 11 | rgba(33, 26, 23, 1) 100% 12 | ); 13 | background-repeat: no-repeat; 14 | background-size: cover; 15 | background-attachment: fixed; 16 | color: white; 17 | -webkit-font-smoothing: antialiased; 18 | min-height: 100vh; 19 | } 20 | 21 | .view { 22 | // margin-left: 50px; 23 | // padding: 10px; 24 | padding-top: 55px; 25 | padding-top: calc(55px + env(safe-area-inset-top)); 26 | 27 | @include media-breakpoint-up(lg) { 28 | margin-left: 250px; 29 | padding-top: 0; 30 | // padding: 20px; 31 | } 32 | } 33 | 34 | #root { 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | bottom: 0; 40 | overflow-y: auto; 41 | height: 100vh; 42 | } 43 | -------------------------------------------------------------------------------- /admin/src/styles/globals/margins.scss: -------------------------------------------------------------------------------- 1 | $margin: 10px; 2 | 3 | .m { 4 | &b { 5 | &--1 { 6 | margin-bottom: #{$margin * 1}; 7 | } 8 | 9 | &--2 { 10 | margin-bottom: #{$margin * 2}; 11 | } 12 | } 13 | 14 | &t { 15 | &--1 { 16 | margin-top: #{$margin * 1}; 17 | } 18 | 19 | &--2 { 20 | margin-top: #{$margin * 2}; 21 | } 22 | } 23 | 24 | &r { 25 | &--1 { 26 | margin-right: #{$margin * 1}; 27 | } 28 | 29 | &--2 { 30 | margin-right: #{$margin * 2}; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /admin/src/styles/globals/page.scss: -------------------------------------------------------------------------------- 1 | .page-wrap { 2 | max-width: 100vw; 3 | overflow-x: hidden; 4 | min-height: 100vh; 5 | position: relative; 6 | padding: 15px; 7 | 8 | @include media-breakpoint-up(lg) { 9 | max-height: 100vh; 10 | overflow-y: auto; 11 | padding: 20px; 12 | } 13 | } 14 | 15 | .page { 16 | display: flex; 17 | flex-wrap: nowrap; 18 | 19 | .sidebar { 20 | width: 100%; 21 | 22 | @include media-breakpoint-up(lg) { 23 | width: 250px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /admin/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | $black: #2a2c2e; 2 | $grey: #f2f2f2; 3 | $grey-light: #e0e3e6; 4 | $grey-medium: #868c96; 5 | $dark-grey: #343536; 6 | $bad: #e95151; 7 | $primary: #d79b23; 8 | $primaryDark: #c78c0d; 9 | $green: #98dd32; 10 | $good: $green; 11 | $blue: #3d85b8; 12 | $purple: rgb(104, 16, 175); 13 | $teal: #1f7979; 14 | $yellow: #ebe723; 15 | 16 | // breakpoints ///////////////////////////////////////////// 17 | 18 | $xs: 0; 19 | $sm: 575px; 20 | $md: 768px; 21 | $lg: 992px; 22 | $xl: 1200px; 23 | $xxl: 1440px; 24 | $xxxl: 1920px; 25 | 26 | $breakpoints: ( 27 | xs: $xs, 28 | sm: $sm, 29 | md: $md, 30 | lg: $lg, 31 | xl: $xl, 32 | xxl: $xxl, 33 | xxxl: $xxxl, 34 | ); 35 | 36 | $container-max-widths: ( 37 | sm: 100%, 38 | md: 100%, 39 | lg: calc(100% - 30px), 40 | xl: calc(100% - 100px), 41 | xxl: calc(100% - 100px), 42 | xxxl: calc(100% - 100px), 43 | ); 44 | 45 | $grid-breakpoints: $breakpoints; 46 | 47 | // dependencies 48 | @import "pre/normalize"; 49 | @import "pre/mixins"; 50 | @import "pre/fonts"; 51 | 52 | // globals 53 | @import "globals/type"; 54 | @import "globals/margins"; 55 | @import "globals/body"; 56 | @import "globals/page"; 57 | @import "globals/sidebar"; 58 | 59 | // components 60 | @import "components/input"; 61 | @import "components/image-upload"; 62 | @import "components/modal"; 63 | @import "components/spinner"; 64 | @import "components/section"; 65 | @import "components/buttons"; 66 | @import "components/card"; 67 | @import "components/carousel"; 68 | @import "components/search-form"; 69 | @import "components/review"; 70 | @import "components/sessions"; 71 | @import "components/table"; 72 | @import "components/sonarr-radarr"; 73 | @import "components/messages"; 74 | 75 | // pages 76 | @import "pages/login"; 77 | @import "pages/setup"; 78 | @import "pages/dashboard"; 79 | @import "pages/settings"; 80 | @import "pages/requests"; 81 | @import "pages/profile"; 82 | -------------------------------------------------------------------------------- /api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/api/.DS_Store -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | config.json -------------------------------------------------------------------------------- /api/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .env 3 | /node_modules/**/* 4 | config.json 5 | petio-rest-linux 6 | petio-rest-macos 7 | *.exe 8 | .DS_Store 9 | config-ash.json 10 | package-lock.json 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /api/fanart/index.js: -------------------------------------------------------------------------------- 1 | // Config 2 | const getConfig = require("../util/config"); 3 | 4 | const request = require("xhr-request"); 5 | 6 | const cacheManager = require("cache-manager"); 7 | const memoryCache = cacheManager.caching({ store: "memory", max: 500, ttl: 86400 /*seconds*/ }); 8 | 9 | async function fanart(id, type) { 10 | let data = false; 11 | try { 12 | data = await memoryCache.wrap(id, function () { 13 | return fanartData(id, type); 14 | }); 15 | } catch (err) { 16 | console.log(err); 17 | } 18 | return data; 19 | } 20 | 21 | async function fanartData(id, type) { 22 | const config = getConfig(); 23 | const fanartApi = config.fanartApi; 24 | let url = `https://webservice.fanart.tv/v3/${type}/${id}?api_key=${fanartApi}`; 25 | return new Promise((resolve, reject) => { 26 | request( 27 | url, 28 | { 29 | method: "GET", 30 | json: true, 31 | }, 32 | function (err, data) { 33 | if (err) { 34 | reject(); 35 | } 36 | // console.log(data); 37 | resolve(data); 38 | } 39 | ); 40 | }); 41 | } 42 | 43 | module.exports = fanart; 44 | -------------------------------------------------------------------------------- /api/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const logger = require("../util/logger"); 4 | 5 | const getConfig = require("../util/config"); 6 | const User = require("../models/user"); 7 | 8 | async function authenticate(req) { 9 | const prefs = getConfig(); 10 | const { authorization: header } = req.headers; 11 | let petioJwt; 12 | if (req.body.authToken) { 13 | petioJwt = req.body.authToken; 14 | } else if (req.cookies && req.cookies.petio_jwt) { 15 | petioJwt = req.cookies.petio_jwt; 16 | } else if (header && /^Bearer (.*)$/.test(header)) { 17 | const match = /^Bearer (.*)$/.exec(header); 18 | petioJwt = match[1]; 19 | } else { 20 | throw `AUTH: No auth token provided - route ${req.path}`; 21 | } 22 | req.jwtUser = jwt.verify(petioJwt, prefs.plexToken); 23 | 24 | try { 25 | let userData = await User.findOne({ id: req.jwtUser.id }); 26 | return userData.toObject(); 27 | } catch { 28 | throw `AUTH: User ${req.jwtUser.id} not found in DB - route ${req.path}`; 29 | } 30 | } 31 | 32 | exports.authenticate = authenticate; 33 | 34 | exports.authRequired = async (req, res, next) => { 35 | try { 36 | await authenticate(req); 37 | } catch (e) { 38 | logger.log("warn", `AUTH: user is not logged in`); 39 | logger.warn(e); 40 | res.sendStatus(401); 41 | return; 42 | } 43 | next(); 44 | }; 45 | 46 | exports.adminRequired = (req, res, next) => { 47 | if (req.jwtUser && req.jwtUser.admin) { 48 | next(); 49 | } else { 50 | res.sendStatus(403); 51 | logger.log("warn", `AUTH: User not admin`); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /api/models/archive.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ArchiveSchema = mongoose.Schema({ 4 | requestId: String, 5 | type: String, 6 | title: String, 7 | thumb: String, 8 | imdb_id: String, 9 | tmdb_id: String, 10 | tvdb_id: String, 11 | users: Array, 12 | sonarrId: Array, 13 | radarrId: Array, 14 | approved: Boolean, 15 | removed: Boolean, 16 | removed_reason: String, 17 | complete: Boolean, 18 | timeStamp: Date, 19 | }); 20 | 21 | module.exports = mongoose.model("Archive", ArchiveSchema); 22 | -------------------------------------------------------------------------------- /api/models/artist.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const MusicSchema = mongoose.Schema( 4 | { 5 | title: String, 6 | ratingKey: Number, 7 | metaId: String, 8 | metaTitle: String, 9 | key: String, 10 | guid: String, 11 | type: String, 12 | summary: String, 13 | index: Number, 14 | thumb: String, 15 | addedAt: Number, 16 | updatedAt: Number, 17 | Genre: Array, 18 | Country: Array, 19 | }, 20 | { collection: "music" } 21 | ); 22 | 23 | module.exports = mongoose.model("Music", MusicSchema); 24 | 25 | // ratingKey 26 | -------------------------------------------------------------------------------- /api/models/discovery.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const DiscoverySchema = mongoose.Schema({ 4 | id: String, 5 | movie: { 6 | genres: Object, 7 | people: { 8 | cast: Object, 9 | director: Object, 10 | }, 11 | history: Object, 12 | }, 13 | series: { 14 | genres: Object, 15 | people: { 16 | cast: Object, 17 | director: Object, 18 | }, 19 | history: Object, 20 | }, 21 | }); 22 | 23 | module.exports = mongoose.model("Discover", DiscoverySchema); 24 | -------------------------------------------------------------------------------- /api/models/filter.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const FilterSchema = mongoose.Schema({ 4 | id: String, 5 | data: Array, 6 | }); 7 | 8 | module.exports = mongoose.model("Filter", FilterSchema); 9 | -------------------------------------------------------------------------------- /api/models/imdb.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ImdbSchema = mongoose.Schema({ 4 | id: String, 5 | rating: String, 6 | }); 7 | 8 | module.exports = mongoose.model("Imdb", ImdbSchema); 9 | -------------------------------------------------------------------------------- /api/models/issue.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const IssueSchema = mongoose.Schema({ 4 | mediaId: String, 5 | type: String, 6 | title: String, 7 | user: String, 8 | sonarrId: Array, 9 | radarrId: Array, 10 | issue: String, 11 | comment: String, 12 | }); 13 | 14 | module.exports = mongoose.model("Issue", IssueSchema); 15 | -------------------------------------------------------------------------------- /api/models/library.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const LibrarySchema = mongoose.Schema( 4 | { 5 | allowSync: Boolean, 6 | art: String, 7 | composite: String, 8 | filters: Boolean, 9 | refreshing: Boolean, 10 | thumb: String, 11 | key: String, 12 | type: String, 13 | title: String, 14 | agent: String, 15 | scanner: String, 16 | language: String, 17 | uuid: String, 18 | updatedAt: Number, 19 | createdAt: Number, 20 | scannedAt: Number, 21 | content: Boolean, 22 | directory: Number, 23 | contentChangedAt: Number, 24 | hidden: String, 25 | }, 26 | { collection: "libraries" } 27 | ); 28 | 29 | module.exports = mongoose.model("Library", LibrarySchema); 30 | 31 | // uuid 32 | -------------------------------------------------------------------------------- /api/models/movie.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const MovieSchema = mongoose.Schema({ 4 | title: String, 5 | ratingKey: Number, 6 | key: String, 7 | guid: String, 8 | studio: String, 9 | type: String, 10 | titleSort: String, 11 | contentRating: String, 12 | summary: String, 13 | rating: Number, 14 | year: Number, 15 | tagline: String, 16 | thumb: String, 17 | art: String, 18 | duration: Number, 19 | originallyAvailableAt: String, 20 | addedAt: Number, 21 | updatedAt: Number, 22 | primaryExtraKey: String, 23 | ratingImage: String, 24 | Media: Array, 25 | Genre: Array, 26 | Director: Array, 27 | Writer: Array, 28 | Country: Array, 29 | Role: Array, 30 | idSource: String, 31 | externalId: String, 32 | imdb_id: String, 33 | tmdb_id: String, 34 | petioTimestamp: Date, 35 | }); 36 | 37 | module.exports = mongoose.model("Movie", MovieSchema); 38 | 39 | // ratingKey 40 | -------------------------------------------------------------------------------- /api/models/profile.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ProfileSchema = mongoose.Schema( 4 | { 5 | name: String, 6 | sonarr: Object, 7 | radarr: Object, 8 | autoApprove: Boolean, 9 | autoApproveTv: Boolean, 10 | quota: Number, 11 | isDefault: Boolean, 12 | }, 13 | { collection: "profiles" } 14 | ); 15 | 16 | module.exports = mongoose.model("Profile", ProfileSchema); 17 | -------------------------------------------------------------------------------- /api/models/request.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const RequestSchema = mongoose.Schema({ 4 | requestId: String, 5 | type: String, 6 | title: String, 7 | thumb: String, 8 | imdb_id: String, 9 | tmdb_id: String, 10 | tvdb_id: String, 11 | users: Array, 12 | sonarrId: Array, 13 | radarrId: Array, 14 | approved: Boolean, 15 | manualStatus: Number, 16 | pendingDefault: Object, 17 | seasons: Object, 18 | timeStamp: Date, 19 | }); 20 | 21 | module.exports = mongoose.model("Request", RequestSchema); 22 | -------------------------------------------------------------------------------- /api/models/review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ReviewSchema = mongoose.Schema( 4 | { 5 | tmdb_id: String, 6 | score: Number, 7 | comment: String, 8 | user: String, 9 | date: Date, 10 | type: String, 11 | title: String, 12 | }, 13 | { collection: "reviews" } 14 | ); 15 | 16 | module.exports = mongoose.model("Review", ReviewSchema); 17 | 18 | // ratingKey 19 | -------------------------------------------------------------------------------- /api/models/show.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const TvSchema = mongoose.Schema( 4 | { 5 | ratingKey: Number, 6 | key: String, 7 | guid: String, 8 | studio: String, 9 | type: String, 10 | title: String, 11 | titleSort: String, 12 | contentRating: String, 13 | summary: String, 14 | index: Number, 15 | rating: Number, 16 | year: Number, 17 | thumb: String, 18 | art: String, 19 | banner: String, 20 | theme: String, 21 | duration: Number, 22 | originallyAvailableAt: String, 23 | leafCount: Number, 24 | viewedLeafCount: Number, 25 | childCount: Number, 26 | addedAt: Number, 27 | updatedAt: Number, 28 | Genre: Array, 29 | idSource: String, 30 | externalId: String, 31 | tvdb_id: String, 32 | imdb_id: String, 33 | tmdb_id: String, 34 | petioTimestamp: Date, 35 | seasonData: Object, 36 | }, 37 | { collection: "shows" } 38 | ); 39 | 40 | module.exports = mongoose.model("Show", TvSchema); 41 | 42 | // ratingKey 43 | -------------------------------------------------------------------------------- /api/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const FriendSchema = mongoose.Schema( 4 | { 5 | id: String, 6 | title: String, 7 | username: String, 8 | nameLower: String, 9 | email: String, 10 | password: String, 11 | recommendationsPlaylistId: String, 12 | thumb: String, 13 | Server: Array, 14 | altId: String, 15 | lastIp: String, 16 | role: String, 17 | profile: String, 18 | custom: Boolean, 19 | disabled: Boolean, 20 | quotaCount: Number, 21 | custom_thumb: String, 22 | lastLogin: Date, 23 | petioTimestamp: Date, 24 | }, 25 | { collection: "friends" } 26 | ); 27 | 28 | module.exports = mongoose.model("Friend", FriendSchema); 29 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petio-rest", 3 | "version": "0.5.7-alpha", 4 | "description": "Rest Api for Petio", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "bcryptjs": "2.4.3", 14 | "bluebird": "^3.7.2", 15 | "cache-manager": "^3.4.1", 16 | "cheerio": "^1.0.0-rc.10", 17 | "cluster": "^0.7.7", 18 | "cookie-parser": "^1.4.5", 19 | "cors": "^2.8.5", 20 | "cron": "^1.8.2", 21 | "express": "^4.17.1", 22 | "express-cache-middleware": "^1.0.1", 23 | "follow-redirects": "^1.13.3", 24 | "fs": "0.0.1-security", 25 | "http": "0.0.1-security", 26 | "https": "^1.0.0", 27 | "iso-639-1": "^2.1.8", 28 | "joi": "^17.3.0", 29 | "jsonwebtoken": "^8.5.1", 30 | "line-reader": "^0.4.0", 31 | "mongoose": "^5.12.1", 32 | "multer": "^1.4.2", 33 | "musicbrainz-api": "^0.6.0", 34 | "nodejs-nodemailer-outlook": "^1.2.4", 35 | "nodemailer": "^6.5.0", 36 | "sanitize-filename": "^1.6.3", 37 | "saslprep": "^1.0.3", 38 | "winston": "^3.3.3", 39 | "xhr-request": "^1.1.0", 40 | "xml-js": "^1.6.11", 41 | "zlib": "^1.0.5" 42 | }, 43 | "devDependencies": { 44 | "dotenv": "^8.2.0" 45 | }, 46 | "bin": "app.js", 47 | "targets": [ 48 | "node14-linux-x64", 49 | "node14-macos-x64", 50 | "node14-win-x64" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /api/plex/bandwidth.js: -------------------------------------------------------------------------------- 1 | const request = require("xhr-request"); 2 | const axios = require("axios"); 3 | 4 | // Config 5 | const getConfig = require("../util/config"); 6 | 7 | async function getBandwidth() { 8 | const prefs = getConfig(); 9 | let url = `${prefs.plexProtocol}://${prefs.plexIp}:${prefs.plexPort}/statistics/bandwidth?timespan=6&X-Plex-Token=${prefs.plexToken}`; 10 | try { 11 | let res = await axios.get(url); 12 | return res.data; 13 | } catch (e) { 14 | // Do nothing 15 | } 16 | } 17 | 18 | module.exports = getBandwidth; 19 | -------------------------------------------------------------------------------- /api/plex/plexLookup.js: -------------------------------------------------------------------------------- 1 | const Movie = require('../models/movie'); 2 | const Show = require('../models/show'); 3 | 4 | async function plexLookup(id, type) { 5 | let plexMatch = false; 6 | if (type === 'movie') { 7 | plexMatch = await Movie.findOne({ 8 | ratingKey: id, 9 | }).exec(); 10 | } else { 11 | plexMatch = await Show.findOne({ 12 | ratingKey: id, 13 | }).exec(); 14 | } 15 | if (!plexMatch) { 16 | return { error: 'not found, invalid key' }; 17 | } else { 18 | return plexMatch; 19 | } 20 | } 21 | 22 | module.exports = plexLookup; 23 | -------------------------------------------------------------------------------- /api/plex/serverInfo.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | // Config 4 | const getConfig = require("../util/config"); 5 | 6 | async function getServerInfo() { 7 | const prefs = getConfig(); 8 | let url = `${prefs.plexProtocol}://${prefs.plexIp}:${prefs.plexPort}/statistics/resources?timespan=6&X-Plex-Token=${prefs.plexToken}`; 9 | try { 10 | let res = await axios.get(url); 11 | return res.data; 12 | } catch (e) { 13 | // Do nothing 14 | } 15 | } 16 | 17 | module.exports = getServerInfo; 18 | -------------------------------------------------------------------------------- /api/plex/sessions.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | // Config 4 | const getConfig = require("../util/config"); 5 | 6 | async function getSessions() { 7 | const prefs = getConfig(); 8 | let url = `${prefs.plexProtocol}://${prefs.plexIp}:${prefs.plexPort}/status/sessions?X-Plex-Token=${prefs.plexToken}`; 9 | try { 10 | let res = await axios.get(url); 11 | return res.data; 12 | } catch (e) { 13 | // Do nothing 14 | } 15 | } 16 | 17 | module.exports = getSessions; 18 | -------------------------------------------------------------------------------- /api/plex/testConnection.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | // Config 4 | 5 | async function testConnection(prot, ip, port, token) { 6 | let url = `${prot}://${ip}:${port}/system?X-Plex-Token=${token}`; 7 | try { 8 | let res = await axios.get(url); 9 | return res.status; 10 | } catch (e) { 11 | // Do nothing 12 | } 13 | } 14 | 15 | module.exports = testConnection; 16 | -------------------------------------------------------------------------------- /api/requests/archive.js: -------------------------------------------------------------------------------- 1 | const Archive = require("../models/archive"); 2 | 3 | async function getArchive(userId) { 4 | const requests = await Archive.find({ users: userId }); 5 | return { requests }; 6 | } 7 | 8 | module.exports = { getArchive }; 9 | -------------------------------------------------------------------------------- /api/requests/quotas.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | const logger = require("../util/logger"); 3 | 4 | class QuotaSystem { 5 | async reset() { 6 | logger.log("info", "QUOTA: Reseting Quotas"); 7 | await User.updateMany({}, { $set: { quotaCount: 0 } }); 8 | } 9 | } 10 | 11 | module.exports = QuotaSystem; 12 | -------------------------------------------------------------------------------- /api/routes/batch.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const logger = require("../util/logger"); 4 | const { movieLookup } = require("../tmdb/movie"); 5 | const showLookup = require("../tmdb/show"); 6 | 7 | router.post("/movie", async (req, res) => { 8 | const ids = req.body.ids; 9 | let output = await Promise.all( 10 | ids.map(async (id) => { 11 | if (!id) return; 12 | const movie = await movieLookup(id, true); 13 | return movie; 14 | }) 15 | ); 16 | res.json(output); 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /api/routes/discovery.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const logger = require("../util/logger"); 4 | const getDiscovery = require("../discovery/display"); 5 | 6 | router.get("/movies", async (req, res) => { 7 | let userId = req.jwtUser.altId ? req.jwtUser.altId : req.jwtUser.id; 8 | if (!userId) { 9 | res.sendStatus(404); 10 | } 11 | try { 12 | logger.log({ 13 | level: "info", 14 | message: `ROUTE: Movie Discovery Profile returned for ${userId}`, 15 | }); 16 | let data = await getDiscovery(userId, "movie"); 17 | if (data.error) throw data.error; 18 | res.json(data); 19 | } catch (err) { 20 | logger.log({ level: "error", message: err }); 21 | res.sendStatus(500); 22 | } 23 | }); 24 | 25 | router.get("/shows", async (req, res) => { 26 | let userId = req.jwtUser.altId ? req.jwtUser.altId : req.jwtUser.id; 27 | if (!userId) { 28 | res.sendStatus(404); 29 | } 30 | try { 31 | logger.log({ 32 | level: "info", 33 | message: `ROUTE: TV Discovery Profile returned for ${userId}`, 34 | }); 35 | let data = await getDiscovery(userId, "show"); 36 | if (data.error) throw data.error; 37 | res.json(data); 38 | } catch (err) { 39 | logger.log({ level: "error", message: err }); 40 | res.sendStatus(500); 41 | } 42 | }); 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /api/routes/history.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const getHistory = require("../plex/history"); 4 | const getBandwidth = require("../plex/bandwidth"); 5 | const getServerInfo = require("../plex/serverInfo"); 6 | const logger = require("../util/logger"); 7 | 8 | router.post("/", async (req, res) => { 9 | let id = req.body.id; 10 | if (id === "admin") id = 1; 11 | try { 12 | let data = await getHistory(id, req.body.type); 13 | res.json(data); 14 | } catch (err) { 15 | logger.log("warn", "ROUTE: Error getting history"); 16 | logger.log({ level: "error", message: err }); 17 | res.status(500).send(); 18 | } 19 | }); 20 | 21 | router.get("/bandwidth", async (req, res) => { 22 | try { 23 | let data = await getBandwidth(); 24 | res.json(data.MediaContainer.StatisticsBandwidth); 25 | } catch (err) { 26 | logger.log("warn", "ROUTE: Error getting bandwidth"); 27 | logger.log({ level: "error", message: err }); 28 | res.status(500).send(); 29 | } 30 | }); 31 | 32 | router.get("/server", async (req, res) => { 33 | try { 34 | let data = await getServerInfo(); 35 | res.json(data.MediaContainer); 36 | } catch (err) { 37 | logger.log("warn", "ROUTE: Error getting server info"); 38 | logger.log({ level: "error", message: err }); 39 | res.status(500).send(); 40 | } 41 | }); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /api/routes/log.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const { adminRequired } = require("../middleware/auth"); 6 | 7 | let liveLogfile = process.pkg 8 | ? path.join(path.dirname(process.execPath), "./logs/live1.log") 9 | : "./logs/live1.log"; 10 | let liveLogfile2 = process.pkg 11 | ? path.join(path.dirname(process.execPath), "./logs/live.log") 12 | : "./logs/live.log"; 13 | 14 | router.use(adminRequired); 15 | router.get("/stream", adminRequired, async (req, res) => { 16 | // res.status(200).send(); 17 | let dataNew, dataOld; 18 | try { 19 | let logsNew = fs.readFileSync(liveLogfile, "utf8"); 20 | dataNew = JSON.parse(`[${logsNew.replace(/,\s*$/, "")}]`); 21 | } catch { 22 | dataNew = []; 23 | } 24 | 25 | try { 26 | let logsOld = fs.readFileSync(liveLogfile2, "utf8"); 27 | dataOld = JSON.parse(`[${logsOld.replace(/,\s*$/, "")}]`); 28 | } catch { 29 | dataOld = []; 30 | } 31 | 32 | let data = [...dataNew, ...dataOld]; 33 | res.json(data); 34 | }); 35 | 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /api/routes/movie.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { movieLookup, discoverMovie, company } = require("../tmdb/movie"); 4 | 5 | router.get("/lookup/:id", async (req, res) => { 6 | let data = await movieLookup(req.params.id); 7 | res.json(data); 8 | }); 9 | 10 | router.get("/lookup/:id/minified", async (req, res) => { 11 | let data = await movieLookup(req.params.id, true); 12 | res.json(data); 13 | }); 14 | 15 | router.post("/discover", async (req, res) => { 16 | let page = req.body.page ? req.body.page : 1; 17 | let params = req.body.params; 18 | let data = await discoverMovie(page, params); 19 | res.json(data); 20 | }); 21 | 22 | router.get("/company/:id", async (req, res) => { 23 | let data = await company(req.params.id); 24 | res.json(data); 25 | }); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /api/routes/notifications.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const logger = require("../util/logger"); 4 | const Discord = require("../notifications/discord"); 5 | const Telegram = require("../notifications/telegram"); 6 | 7 | router.get("/discord/test", async (req, res) => { 8 | let test = await new Discord().test(); 9 | res.json({ result: test.result, error: test.error }); 10 | }); 11 | 12 | router.get("/telegram/test", async (req, res) => { 13 | let test = await new Telegram().test(); 14 | res.json({ result: test.result, error: test.error }); 15 | }); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /api/routes/person.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const personLookup = require("../tmdb/person"); 4 | 5 | const ExpressCache = require("express-cache-middleware"); 6 | const cacheManager = require("cache-manager"); 7 | 8 | // Cache for 1 day 9 | const cacheMiddleware = new ExpressCache( 10 | cacheManager.caching({ 11 | store: "memory", 12 | max: 100, 13 | ttl: 86400, 14 | }) 15 | ); 16 | 17 | cacheMiddleware.attach(router); 18 | 19 | router.get("/lookup/:id", async (req, res) => { 20 | let data = await personLookup(req.params.id); 21 | res.json(data); 22 | }); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /api/routes/review.js: -------------------------------------------------------------------------------- 1 | require("dotenv/config"); 2 | 3 | const express = require("express"); 4 | const router = express.Router(); 5 | const Review = require("../models/review"); 6 | const User = require("../models/user"); 7 | 8 | router.post("/add", async (req, res) => { 9 | let item = req.body.item; 10 | let review = req.body.review; 11 | let user = req.body.user; 12 | let userData = await User.findOne({ id: user }); 13 | 14 | try { 15 | const newReview = new Review({ 16 | tmdb_id: item.id, 17 | score: review.score, 18 | comment: review.comment, 19 | user: userData.id, 20 | date: new Date(), 21 | type: item.type, 22 | title: item.title, 23 | }); 24 | 25 | const savedReview = await newReview.save(); 26 | res.json(savedReview); 27 | } catch (err) { 28 | res.status(500).json({ error: err }); 29 | } 30 | }); 31 | 32 | router.get("/all", async (req, res) => { 33 | try { 34 | const reviews = await Review.find(); 35 | res.json(reviews); 36 | } catch (err) { 37 | res.status(500).json({ error: err }); 38 | } 39 | }); 40 | 41 | router.get("/all/:id", async (req, res) => { 42 | let id = req.params.id; 43 | if (!id) { 44 | res.status(500).json({ error: "ID required" }); 45 | return; 46 | } 47 | try { 48 | const reviews = await Review.find({ tmdb_id: id }); 49 | res.json(reviews); 50 | } catch (err) { 51 | res.status(500).json({}); 52 | } 53 | }); 54 | 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /api/routes/search.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const search = require("../tmdb/search"); 4 | const MusicMeta = require("../meta/musicBrainz"); 5 | const ExpressCache = require("express-cache-middleware"); 6 | const cacheManager = require("cache-manager"); 7 | 8 | // Cache for 1 day 9 | const cacheMiddleware = new ExpressCache( 10 | cacheManager.caching({ 11 | store: "memory", 12 | max: 100, 13 | ttl: 86400, 14 | }) 15 | ); 16 | 17 | // Caching not applied needs setting up 18 | 19 | router.get("/:term", async (req, res) => { 20 | try { 21 | let data = await search(req.params.term.replace(/[^a-zA-Z0-9 ]/g, "")); 22 | res.json(data); 23 | } catch (err) { 24 | console.log(err); 25 | res.json({ 26 | movies: [], 27 | people: [], 28 | shows: [], 29 | }); 30 | } 31 | }); 32 | 33 | router.get("/music/:term", async (req, res) => { 34 | new MusicMeta().search(req.params.term); 35 | }); 36 | 37 | module.exports = router; 38 | -------------------------------------------------------------------------------- /api/routes/sessions.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const getSessions = require("../plex/sessions"); 4 | const logger = require("../util/logger"); 5 | const { adminRequired } = require("../middleware/auth"); 6 | 7 | router.use(adminRequired); 8 | router.get("/", async (req, res) => { 9 | try { 10 | let data = await getSessions(); 11 | res.json(data.MediaContainer); 12 | } catch (err) { 13 | logger.log("warn", "ROUTE: Unable to get sessions"); 14 | logger.log({ level: "error", message: err }); 15 | res.status(500).send(); 16 | } 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /api/routes/show.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { showLookup, discoverSeries, network } = require("../tmdb/show"); 4 | 5 | router.get("/lookup/:id", async (req, res) => { 6 | let data = await showLookup(req.params.id, false); 7 | res.json(data); 8 | }); 9 | 10 | router.get("/lookup/:id/minified", async (req, res) => { 11 | let data = await showLookup(req.params.id, true); 12 | res.json(data); 13 | }); 14 | 15 | router.post("/discover", async (req, res) => { 16 | let page = req.body.page ? req.body.page : 1; 17 | let params = req.body.params; 18 | let data = await discoverSeries(page, params); 19 | res.json(data); 20 | }); 21 | 22 | router.get("/network/:id", async (req, res) => { 23 | let data = await network(req.params.id); 24 | res.json(data); 25 | }); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /api/routes/top.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const getTop = require("../plex/top"); 4 | 5 | const ExpressCache = require("express-cache-middleware"); 6 | const cacheManager = require("cache-manager"); 7 | 8 | // Cache for 1 day 9 | const cacheMiddleware = new ExpressCache( 10 | cacheManager.caching({ 11 | store: "memory", 12 | max: 100, 13 | ttl: 86400, 14 | }) 15 | ); 16 | 17 | cacheMiddleware.attach(router); 18 | 19 | router.get("/movies", async (req, res) => { 20 | let data = await getTop(1); 21 | res.json(data); 22 | }); 23 | 24 | router.get("/shows", async (req, res) => { 25 | let data = await getTop(2); 26 | res.json(data); 27 | }); 28 | 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /api/routes/trending.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const trending = require("../tmdb/trending"); 4 | 5 | router.get("/", async (req, res) => { 6 | let data = await trending(); 7 | res.json(data); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /api/tmdb/person.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const agent = new http.Agent({ family: 4 }); 3 | const axios = require("axios"); 4 | 5 | // Config 6 | const getConfig = require("../util/config"); 7 | const logger = require("../util/logger"); 8 | 9 | async function personLookup(id) { 10 | logger.log("verbose", `TMDB Person Lookup ${id}`); 11 | let info = await getPersonInfo(id); 12 | let movies = await getPersonMovies(id); 13 | let tv = await getPersonShows(id); 14 | 15 | let person = { 16 | info: info, 17 | movies: movies, 18 | tv: tv, 19 | }; 20 | 21 | return person; 22 | } 23 | 24 | async function getPersonInfo(id) { 25 | const config = getConfig(); 26 | const tmdbApikey = config.tmdbApi; 27 | const tmdb = "https://api.themoviedb.org/3/"; 28 | let url = `${tmdb}person/${id}?api_key=${tmdbApikey}&append_to_response=images`; 29 | try { 30 | let res = await axios.get(url, { httpAgent: agent }); 31 | return res.data; 32 | } catch (err) { 33 | throw err; 34 | } 35 | } 36 | 37 | async function getPersonMovies(id) { 38 | const config = getConfig(); 39 | const tmdbApikey = config.tmdbApi; 40 | const tmdb = "https://api.themoviedb.org/3/"; 41 | let url = `${tmdb}person/${id}/movie_credits?api_key=${tmdbApikey}&append_to_response=credits,videos`; 42 | try { 43 | let res = await axios.get(url, { httpAgent: agent }); 44 | return res.data; 45 | } catch (err) { 46 | throw err; 47 | } 48 | } 49 | 50 | async function getPersonShows(id) { 51 | const config = getConfig(); 52 | const tmdbApikey = config.tmdbApi; 53 | const tmdb = "https://api.themoviedb.org/3/"; 54 | let url = `${tmdb}person/${id}/tv_credits?api_key=${tmdbApikey}&append_to_response=credits,videos`; 55 | try { 56 | let res = await axios.get(url, { httpAgent: agent }); 57 | return res.data; 58 | } catch (err) { 59 | throw err; 60 | } 61 | } 62 | 63 | module.exports = personLookup; 64 | -------------------------------------------------------------------------------- /api/util/config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const logger = require("./logger"); 4 | 5 | function getConfig() { 6 | let project_folder, configFile; 7 | if (process.pkg) { 8 | project_folder = path.dirname(process.execPath); 9 | configFile = path.join(project_folder, "./config/config.json"); 10 | } else { 11 | project_folder = __dirname; 12 | configFile = path.join(project_folder, "../config/config.json"); 13 | } 14 | 15 | let userConfig = false; 16 | try { 17 | userConfig = fs.readFileSync(configFile); 18 | return JSON.parse(userConfig); 19 | } catch (err) { 20 | logger.log("error", "Unable to get config - Config Not Found! Exiting"); 21 | return false; 22 | } 23 | } 24 | 25 | module.exports = getConfig; 26 | -------------------------------------------------------------------------------- /api/util/logger.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | const winston = require("winston"); 3 | 4 | let logfile = process.pkg 5 | ? path.join(path.dirname(process.execPath), "./logs/logfile.log") 6 | : "./logs/logfile.log"; 7 | let liveLogfile = process.pkg 8 | ? path.join(path.dirname(process.execPath), "./logs/live.log") 9 | : "./logs/live.log"; 10 | 11 | const logger = winston.createLogger({ 12 | transports: [ 13 | new winston.transports.Console({ 14 | format: winston.format.combine( 15 | winston.format.colorize(), 16 | winston.format.timestamp({ 17 | format: "YYYY-MM-DD HH:mm:ss", 18 | }), 19 | winston.format.printf( 20 | (info) => `${info.timestamp} ${info.level}: ${info.message}` 21 | ) 22 | ), 23 | }), 24 | new winston.transports.File({ 25 | level: "silly", 26 | filename: logfile, 27 | maxsize: 1000000, 28 | maxFiles: 10, 29 | format: winston.format.combine( 30 | winston.format.timestamp(), 31 | winston.format.printf( 32 | (info) => `${info.timestamp} ${info.level}: ${info.message}` 33 | ) 34 | ), 35 | }), 36 | new winston.transports.File({ 37 | filename: liveLogfile, 38 | level: "silly", 39 | maxsize: 100000, 40 | maxFiles: 1, 41 | tailable: true, 42 | format: winston.format.combine( 43 | winston.format.timestamp(), 44 | winston.format.printf((info) => { 45 | return `${JSON.stringify({ 46 | [info.timestamp]: { 47 | type: info.level, 48 | log: info.message, 49 | }, 50 | })},`; 51 | }) 52 | ), 53 | }), 54 | ], 55 | }); 56 | 57 | module.exports = logger; 58 | -------------------------------------------------------------------------------- /api/util/setupReady.js: -------------------------------------------------------------------------------- 1 | const Movie = require("../models/movie"); 2 | const Show = require("../models/show"); 3 | const User = require("../models/user"); 4 | const logger = require("./logger"); 5 | 6 | async function setupReady() { 7 | try { 8 | let [movie, show, user] = await Promise.all([ 9 | Movie.findOne(), 10 | Show.findOne(), 11 | User.findOne(), 12 | ]); 13 | if (movie && show && user) { 14 | return { 15 | ready: true, 16 | error: false, 17 | }; 18 | } else { 19 | return { 20 | ready: false, 21 | error: false, 22 | }; 23 | } 24 | } catch { 25 | logger.error("CHK: Fatal Error unable to write Db to file!"); 26 | return { 27 | ready: false, 28 | error: "Database write error", 29 | }; 30 | } 31 | } 32 | 33 | module.exports = setupReady; 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | networks: 4 | petio-network: 5 | driver: bridge 6 | 7 | services: 8 | petio: 9 | image: ghcr.io/petio-team/petio:latest 10 | container_name: "petio" 11 | hostname: petio 12 | ports: 13 | - "7777:7777" 14 | networks: 15 | - petio-network 16 | depends_on: 17 | - mongo 18 | user: "1000:1000" 19 | environment: 20 | - TZ=Etc/UTC 21 | volumes: 22 | - ./config:/app/api/config 23 | - ./logs:/app/logs 24 | 25 | mongo: 26 | image: mongo:latest 27 | container_name: "mongo" 28 | hostname: mongo 29 | ports: 30 | - "27017:27017" 31 | networks: 32 | - petio-network 33 | user: "1000:1000" 34 | volumes: 35 | - ./db:/data/db 36 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{package,bower}.json] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [{.eslintrc,.scss-lint.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.{scss,sass}] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["react", "prettier"], 15 | "rules": { 16 | "react/prop-types": 0, 17 | "no-class-assign": 0, 18 | "no-useless-catch": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | ash-config.json 24 | package-lock.json 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Petio Request Front End 2 | 3 | React / Redux 4 | 5 | Status: Alpha prototype released 0.1.7 6 | 7 | To run a build run yarn / npm build. 8 | 9 | To run a webpack preview run yarn / npm start. 10 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petio", 3 | "version": "0.5.7-alpha", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^13.1.9", 10 | "axios": "^0.21.1", 11 | "dateformat": "^4.5.1", 12 | "lodash": "^4.17.21", 13 | "moment": "^2.29.1", 14 | "react": "^17.0.2", 15 | "react-big-calendar": "^0.33.5", 16 | "react-device-detect": "^1.17.0", 17 | "react-dom": "^17.0.2", 18 | "react-lazy-load-image-component": "^1.5.1", 19 | "react-player": "^2.9.0", 20 | "react-redux": "^7.2.4", 21 | "react-router": "^5.2.0", 22 | "react-router-dom": "^5.1.2", 23 | "react-scripts": "4.0.3", 24 | "redux": "^4.1.0", 25 | "redux-devtools-extension": "^2.13.9", 26 | "redux-thunk": "^2.3.0", 27 | "sass": "1.34.1" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | "since 2010" 41 | ], 42 | "development": [ 43 | "since 2010" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "eslint": "^7.29.0", 48 | "eslint-config-prettier": "^8.3.0", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "eslint-plugin-react": "^7.24.0", 51 | "prettier": "^2.3.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "PlexRequestApi": "", 3 | "PlexRequestApiPort": "7778" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /frontend/public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /frontend/public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /frontend/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /frontend/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /frontend/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /frontend/public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /frontend/public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /frontend/public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /frontend/public/fonts/Khula-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/fonts/Khula-Bold.eot -------------------------------------------------------------------------------- /frontend/public/fonts/Khula-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/fonts/Khula-Bold.ttf -------------------------------------------------------------------------------- /frontend/public/fonts/Khula-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/fonts/Khula-Bold.woff -------------------------------------------------------------------------------- /frontend/public/fonts/Khula-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/fonts/Khula-Bold.woff2 -------------------------------------------------------------------------------- /frontend/public/images/no-poster-person.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/no-poster-person.jpg -------------------------------------------------------------------------------- /frontend/public/images/no-poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/no-poster.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /frontend/public/images/splash/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/images/splash/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Petio", 3 | "name": "Petio Plex Request", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-icon-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image/png", 9 | "density": "0.75" 10 | }, 11 | { 12 | "src": "/favicon/android-icon-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image/png", 15 | "density": "1.0" 16 | }, 17 | { 18 | "src": "/favicon/android-icon-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image/png", 21 | "density": "1.5" 22 | }, 23 | { 24 | "src": "/favicon/android-icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image/png", 27 | "density": "2.0" 28 | }, 29 | { 30 | "src": "/favicon/android-icon-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image/png", 33 | "density": "3.0" 34 | }, 35 | { 36 | "src": "/favicon/android-icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png", 39 | "density": "4.0" 40 | } 41 | ], 42 | "start_url": ".", 43 | "display": "standalone", 44 | "theme_color": "#3f4245", 45 | "background_color": "#3f4245" 46 | } 47 | -------------------------------------------------------------------------------- /frontend/public/p-seamless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/p-seamless.png -------------------------------------------------------------------------------- /frontend/public/petio_splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/public/petio_splash.jpg -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Khula-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/src/assets/fonts/Khula-Bold.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Khula-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/src/assets/fonts/Khula-Bold.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Khula-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/src/assets/fonts/Khula-Bold.woff -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Khula-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petio-team/petio/abda13502a8977fa3aa098ec2157bb1a68fe9060/frontend/src/assets/fonts/Khula-Bold.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/svg/720p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/admin.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/bookmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/action.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/adventure.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/animation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/anime.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/comedy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/crime.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/documentary.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/drama.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/family.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/fantasy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/history.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/horror.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/mystery.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/romance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/science-fiction.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/thriller.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/tv-movie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/war.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/genres/western.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/movie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/person-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/report.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/request.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/tv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/data/Api/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPopular, 3 | movie, 4 | series, 5 | search, 6 | clearSearch, 7 | person, 8 | top, 9 | history, 10 | get_plex_media, 11 | checkConfig, 12 | discover, 13 | networkDetails, 14 | companyDetails, 15 | guideCalendar, 16 | discoveryMovies, 17 | discoveryShows, 18 | batchLookup, 19 | } from "./actions"; 20 | 21 | export default { 22 | getPopular, 23 | movie, 24 | series, 25 | search, 26 | clearSearch, 27 | person, 28 | top, 29 | history, 30 | get_plex_media, 31 | checkConfig, 32 | discover, 33 | networkDetails, 34 | companyDetails, 35 | guideCalendar, 36 | discoveryMovies, 37 | discoveryShows, 38 | batchLookup, 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/src/data/Nav/index.js: -------------------------------------------------------------------------------- 1 | import { store } from "../store"; 2 | import * as types from "../actionTypes"; 3 | 4 | function storeNav(path, state, scroll, carousels = []) { 5 | if (!state) state = {}; 6 | state.getPos = true; 7 | store.dispatch({ 8 | type: types.STORE_NAV, 9 | path: path, 10 | state: state, 11 | scroll: scroll, 12 | carousels: carousels, 13 | }); 14 | } 15 | 16 | function clearNav() { 17 | store.dispatch({ 18 | type: types.CLEAR_NAV, 19 | }); 20 | } 21 | 22 | function getNav(path) { 23 | let state = store.getState(); 24 | return state.nav.pages[path] || false; 25 | } 26 | 27 | export default { storeNav, getNav, clearNav }; 28 | -------------------------------------------------------------------------------- /frontend/src/data/Nav/reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes"; 2 | 3 | export default function ( 4 | state = { 5 | pages: {}, 6 | }, 7 | action 8 | ) { 9 | switch (action.type) { 10 | case types.STORE_NAV: 11 | return { 12 | ...state, 13 | pages: { 14 | ...state.pages, 15 | [action.path]: { 16 | state: action.state, 17 | scroll: action.scroll, 18 | carousels: action.carousels, 19 | }, 20 | }, 21 | }; 22 | 23 | case types.CLEAR_NAV: 24 | return { 25 | ...state, 26 | pages: {}, 27 | }; 28 | 29 | default: 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/data/Plex/api.js: -------------------------------------------------------------------------------- 1 | const plexHeaders = { 2 | "Content-Type": "application/json", 3 | Accept: "application/json", 4 | "X-Plex-Device": "API", 5 | "X-Plex-Device-Name": "Petio", 6 | "X-Plex-Product": "Petio", 7 | "X-Plex-Version": "v1.0", 8 | "X-Plex-Platform-Version": "v1.0", 9 | "X-Plex-Client-Identifier": "df9e71a5-a6cd-488e-8730-aaa9195f7435", 10 | }; 11 | 12 | export function getPins() { 13 | let url = "https://plex.tv/api/v2/pins?strong=true"; 14 | let method = "post"; 15 | let headers = plexHeaders; 16 | console.log(headers); 17 | return process(url, headers, method).then((response) => response.json()); 18 | } 19 | 20 | export function validatePin(id) { 21 | let url = `https://plex.tv/api/v2/pins/${id}`; 22 | let method = "get"; 23 | let headers = plexHeaders; 24 | return process(url, headers, method).then((response) => response.json()); 25 | } 26 | 27 | function process(url, headers, method, body = null) { 28 | let args = { 29 | method: method, 30 | headers: headers, 31 | }; 32 | 33 | if (method === "post") { 34 | args.body = body; 35 | } 36 | 37 | return fetch(url, args); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/data/User/api.js: -------------------------------------------------------------------------------- 1 | import { get, post } from "../http"; 2 | 3 | export async function login(user, token = false) { 4 | return post("/login", { user, authToken: token }); 5 | } 6 | 7 | export async function plexLogin(token = false) { 8 | return post("/login/plex_login", { token: token }); 9 | } 10 | 11 | export async function request(req, user) { 12 | return post("/request/add", { request: req, user }); 13 | } 14 | 15 | export async function review(item, id, review) { 16 | let itemMin = { 17 | title: item.title ? item.title : item.name, 18 | type: item.episode_run_time ? "tv" : "movie", 19 | thumb: item.thumb, 20 | id: item.id, 21 | }; 22 | return post("/review/add", { 23 | item: itemMin, 24 | user: id, 25 | review: review, 26 | }); 27 | } 28 | 29 | export async function getRequests() { 30 | return get("/request/min"); 31 | } 32 | 33 | export async function getArchive(id) { 34 | return get(`/request/archive/${id}`); 35 | } 36 | 37 | export async function getReviews(id) { 38 | if (!id) return Promise.resolve(); 39 | return get(`/review/all/${id}`); 40 | } 41 | 42 | export async function addIssue(issue) { 43 | return post("/issue/add", issue); 44 | } 45 | 46 | export async function myRequests() { 47 | return get("/request/me"); 48 | } 49 | 50 | export async function quota() { 51 | return get("/user/quota"); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/data/User/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | login, 3 | logout, 4 | request, 5 | getRequests, 6 | review, 7 | getReviews, 8 | addIssue, 9 | myRequests, 10 | quota, 11 | plexAuth, 12 | getArchive, 13 | } from "./actions"; 14 | 15 | export default { 16 | login, 17 | logout, 18 | request, 19 | getRequests, 20 | review, 21 | getReviews, 22 | addIssue, 23 | myRequests, 24 | quota, 25 | plexAuth, 26 | getArchive, 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/data/User/reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes"; 2 | 3 | export default function ( 4 | state = { 5 | current: false, 6 | logged_in: false, 7 | credentials: false, 8 | library_index: false, 9 | requests: false, 10 | email: false, 11 | }, 12 | action 13 | ) { 14 | switch (action.type) { 15 | case types.LOGIN: 16 | return { 17 | ...state, 18 | current: action.data.user, 19 | logged_in: true, 20 | }; 21 | 22 | case types.LOGOUT: 23 | return { 24 | ...state, 25 | current: false, 26 | logged_in: false, 27 | credentials: false, 28 | }; 29 | 30 | case types.CREDENTIALS: 31 | return { 32 | ...state, 33 | credentials: action.credentials, 34 | }; 35 | 36 | case types.CREDENTIALS_EMAIL: 37 | return { 38 | ...state, 39 | email: action.credentials, 40 | }; 41 | 42 | case types.LIBRARIES_INDEX: 43 | return { 44 | ...state, 45 | library_index: action.libraries, 46 | }; 47 | 48 | case types.LOGIN_ADMIN: 49 | return { 50 | ...state, 51 | logged_in: true, 52 | credentials: { 53 | plexToken: action.credentials.token, 54 | }, 55 | current: action.credentials.username, 56 | }; 57 | 58 | case types.GET_REQUESTS: 59 | return { 60 | ...state, 61 | requests: action.requests, 62 | }; 63 | 64 | case types.GET_REVIEWS: 65 | return { 66 | ...state, 67 | reviews: { 68 | ...state.reviews, 69 | [action.id]: action.reviews, 70 | }, 71 | }; 72 | 73 | case types.UPDATE_QUOTA: 74 | return { 75 | ...state, 76 | current: { 77 | ...state.current, 78 | quotaCount: action.quota, 79 | }, 80 | }; 81 | 82 | default: 83 | return state; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/data/actionTypes.js: -------------------------------------------------------------------------------- 1 | // Plex 2 | export const PLEX_TOKEN = "PLEX_TOKEN"; 3 | 4 | // MB 5 | export const MB_LOGIN = "MB_LOGIN"; 6 | export const MB_USER_ROLES = "MB_USER_ROLES"; 7 | export const MB_CONFIG_SETUP_START = "MB_CONFIG_SETUP_START"; 8 | export const MB_CONFIG_SETUP_END = "MB_CONFIG_SETUP_END"; 9 | export const MB_CONFIG_LOADED = "MB_CONFIG_LOADED"; 10 | export const MB_ACTIVE_SERVER = "MB_ACTIVE_SERVER"; 11 | 12 | // Metadata 13 | export const POPULAR = "POPULAR"; 14 | export const SEARCH = "SEARCH"; 15 | export const MOVIE_LOOKUP = "MOVIE_LOOKUP"; 16 | export const MOVIE_LOOKUP_BATCH = "MOVIE_LOOKUP_BATCH"; 17 | export const SERIES_LOOKUP = "SERIES_LOOKUP"; 18 | export const SERIES_LOOKUP_BATCH = "SERIES_LOOKUP_BATCH"; 19 | export const PERSON_LOOKUP = "PERSON_LOOKUP"; 20 | export const SEASON_LOOKUP = "SEASON_LOOKUP"; 21 | export const STORE_ACTOR_MOVIE = "STORE_ACTOR_MOVIE"; 22 | export const STORE_ACTOR_SERIES = "STORE_ACTOR_SERIES"; 23 | 24 | export const LIBRARIES_INDEX = "LIBRARIES_INDEX"; 25 | export const LIBRARY_ALLOWED = "LIBRARY_ALLOWED"; 26 | 27 | // Request 28 | export const GET_REQUESTS = "GET_REQUESTS"; 29 | 30 | // Reviews 31 | export const GET_REVIEWS = "GET_REVIEWS"; 32 | 33 | // User 34 | export const LOGIN_ADMIN = "LOGIN_ADMIN"; 35 | export const LOGIN = "LOGIN"; 36 | export const LOGOUT = "LOGOUT"; 37 | export const CREDENTIALS = "CREDENTIALS"; 38 | export const CREDENTIALS_EMAIL = "CREDENTIALS_EMAIL"; 39 | export const UPDATE_QUOTA = "UPDATE_QUOTA"; 40 | 41 | // Nav 42 | export const STORE_NAV = "STORE_NAV"; 43 | export const CLEAR_NAV = "CLEAR_NAV"; 44 | -------------------------------------------------------------------------------- /frontend/src/data/auth/index.js: -------------------------------------------------------------------------------- 1 | import { store } from "../store"; 2 | import * as types from "../actionTypes"; 3 | 4 | function stripTrailingSlash(str) { 5 | if (str.substr(-1) === "/") { 6 | return str.substr(0, str.length - 1); 7 | } 8 | return str; 9 | } 10 | 11 | const PlexRequestApi = 12 | process.env.NODE_ENV === "development" 13 | ? "http://localhost:7778" 14 | : `${window.location.protocol}//${window.location.host}${window.location.pathname === "/" ? "" : stripTrailingSlash(window.location.pathname)}/api`; 15 | 16 | export function initAuth() { 17 | finalise({ 18 | type: types.CREDENTIALS, 19 | credentials: { 20 | api: PlexRequestApi, 21 | }, 22 | }); 23 | } 24 | 25 | export function getAuth() { 26 | return store.getState().user.credentials; 27 | } 28 | 29 | function finalise(data = false) { 30 | if (!data) return false; 31 | return store.dispatch(data); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/data/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import api from "./Api/reducer"; 3 | import user from "./User/reducer"; 4 | import nav from "./Nav/reducer"; 5 | 6 | const rootReducer = combineReducers({ 7 | api, 8 | user, 9 | nav, 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /frontend/src/data/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import rootReducer from './reducers'; 3 | import thunk from 'redux-thunk'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | 6 | var store; 7 | 8 | function initStore(initialState) { 9 | store = createStore( 10 | rootReducer, 11 | composeWithDevTools(), 12 | initialState, 13 | applyMiddleware(thunk) 14 | ); 15 | } 16 | 17 | export { store, initStore }; 18 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | import { initStore, store } from "./data/store"; 6 | import { Provider } from "react-redux"; 7 | import "./styles/main.scss"; 8 | import { initAuth } from "./data/auth"; 9 | 10 | const startApp = () => { 11 | initStore(); 12 | initAuth(); 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | }; 20 | 21 | if (!window.cordova) { 22 | startApp(); 23 | } else { 24 | document.addEventListener("deviceready", startApp, false); 25 | } 26 | serviceWorker.unregister(); 27 | -------------------------------------------------------------------------------- /frontend/src/pages/People.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Api from "../data/Api"; 3 | import Carousel from "../components/Carousel"; 4 | import PersonCard from "../components/PersonCard"; 5 | import { withRouter } from "react-router-dom"; 6 | import { connect } from "react-redux"; 7 | // import CarouselLoading from "../components/CarouselLoading"; 8 | // import CarouselLoadingPerson from "../components/CarouselLoadingPerson"; 9 | 10 | class People extends React.Component { 11 | componentDidMount() { 12 | Api.getPopular(); 13 | } 14 | 15 | render() { 16 | return ( 17 | <> 18 |
19 |

People

20 |

Discover Actors, Cast & Crew

21 |
22 |
23 |

Trending People

24 | 25 | {Object.keys(this.props.api.popular).length > 0 26 | ? this.props.api.popular.people.map((person) => { 27 | return ; 28 | }) 29 | : null} 30 | 31 |
32 | 33 | ); 34 | } 35 | } 36 | 37 | People = withRouter(People); 38 | 39 | function PeopleContainer(props) { 40 | return ; 41 | } 42 | 43 | const mapStateToProps = function (state) { 44 | return { 45 | api: state.api, 46 | }; 47 | }; 48 | 49 | export default connect(mapStateToProps)(PeopleContainer); 50 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/src/styles/components/input.scss: -------------------------------------------------------------------------------- 1 | .styled-input { 2 | &--select { 3 | background: rgba($grey-medium, 0.3); 4 | height: 40px; 5 | display: flex; 6 | align-items: center; 7 | padding: 0 10px; 8 | margin-bottom: 20px; 9 | border-radius: 3px; 10 | border: solid 1px rgba($grey-medium, 0.5) !important; 11 | color: white; 12 | 13 | &.disabled { 14 | opacity: 0.5; 15 | cursor: no-drop; 16 | 17 | select { 18 | pointer-events: none; 19 | } 20 | } 21 | 22 | select { 23 | appearance: none; 24 | background: none; 25 | color: white; 26 | font-size: 16px; 27 | outline: none !important; 28 | border: none; 29 | } 30 | 31 | option { 32 | color: black; 33 | background: white; 34 | } 35 | } 36 | 37 | &--textarea { 38 | background: rgba($grey-medium, 0.3); 39 | height: 120px; 40 | min-height: 120px; 41 | padding: 10px; 42 | color: white; 43 | border: solid 1px rgba($grey-medium, 0.5) !important; 44 | border-radius: 3px; 45 | outline: none !important; 46 | font-size: 16px; 47 | min-width: 100%; 48 | max-width: 100%; 49 | } 50 | 51 | &--input { 52 | background: rgba($grey-medium, 0.3); 53 | height: 40px; 54 | padding: 0 10px; 55 | margin-bottom: 20px; 56 | border-radius: 3px; 57 | color: white; 58 | line-height: 40px; 59 | font-size: 16px; 60 | border: solid 1px rgba($grey-medium, 0.5) !important; 61 | outline: none !important; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/styles/components/issues.scss: -------------------------------------------------------------------------------- 1 | // 2 | 3 | .issues { 4 | &--wrap { 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: 999; 9 | width: 100%; 10 | height: 100vh; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | opacity: 0; 15 | pointer-events: none; 16 | transition: all 0.3s ease; 17 | 18 | @include media-breakpoint-up(lg) { 19 | background: rgba(0, 0, 0, 0.6); 20 | } 21 | 22 | &.active { 23 | opacity: 1; 24 | pointer-events: all; 25 | } 26 | } 27 | 28 | &--inner { 29 | width: 500px; 30 | max-width: calc(100% - 40px); 31 | background: $black; 32 | border-radius: 5px; 33 | overflow: hidden; 34 | } 35 | 36 | &--top { 37 | background: $primary; 38 | padding: 10px 20px; 39 | 40 | h3 { 41 | font-size: 15px; 42 | text-transform: uppercase; 43 | letter-spacing: 1px; 44 | } 45 | } 46 | 47 | &--main { 48 | padding: 20px; 49 | 50 | select, 51 | input, 52 | textarea { 53 | width: 100%; 54 | } 55 | 56 | .save-issue { 57 | margin-left: 10px; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/styles/components/messages.scss: -------------------------------------------------------------------------------- 1 | .msg { 2 | padding: 5px 10px; 3 | background: $primary; 4 | color: white; 5 | font-weight: 300; 6 | font-size: 12px; 7 | line-height: 18px; 8 | margin: 0; 9 | 10 | &__input { 11 | margin-top: -20px; 12 | margin-bottom: 20px; 13 | border-bottom-left-radius: 5px; 14 | border-bottom-right-radius: 5px; 15 | } 16 | 17 | &__error { 18 | background: $bad; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/styles/components/push-msg.scss: -------------------------------------------------------------------------------- 1 | .push-msg { 2 | &--wrap { 3 | position: fixed; 4 | bottom: 60px; 5 | right: 10px; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: flex-end; 9 | z-index: 1000; 10 | 11 | @include media-breakpoint-up(lg) { 12 | bottom: 10px; 13 | right: 10px; 14 | } 15 | } 16 | 17 | &--item { 18 | width: 200px; 19 | padding: 10px 20px; 20 | background: $dark-grey; 21 | border-radius: 5px; 22 | margin-top: 5px; 23 | border-left: solid 5px $blue; 24 | 25 | @include media-breakpoint-up(lg) { 26 | width: 300px; 27 | padding: 20px 20px; 28 | margin-top: 10px; 29 | } 30 | 31 | &.good { 32 | border-color: $good; 33 | } 34 | 35 | &.error { 36 | border-color: $bad; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/styles/components/search-form.scss: -------------------------------------------------------------------------------- 1 | .search-form { 2 | display: flex; 3 | align-items: center; 4 | width: 400px; 5 | max-width: 100%; 6 | border-radius: 50px; 7 | overflow: hidden; 8 | 9 | input { 10 | height: 40px; 11 | border: none; 12 | background: rgba(255, 255, 255, 0.1); 13 | padding: 0 0 0 20px; 14 | font-size: 16px; 15 | border-radius: 0 !important; 16 | border: none; 17 | outline: none !important; 18 | color: white; 19 | width: 100%; 20 | 21 | &::placeholder { 22 | color: rgba(255, 255, 255, 0.2); 23 | } 24 | } 25 | 26 | &--clear { 27 | height: 40px; 28 | background: rgba(255, 255, 255, 0.1); 29 | border: none; 30 | outline: none !important; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | 35 | svg { 36 | height: 20px; 37 | width: auto; 38 | opacity: 0; 39 | 40 | path { 41 | fill: rgba(255, 255, 255, 0.2); 42 | } 43 | } 44 | 45 | &.active { 46 | cursor: pointer; 47 | svg { 48 | opacity: 1; 49 | } 50 | } 51 | } 52 | 53 | .search-btn { 54 | width: 50px; 55 | height: 40px; 56 | background: rgba(255, 255, 255, 0.1); 57 | border: none; 58 | outline: none !important; 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | 63 | svg { 64 | height: 20px; 65 | width: auto; 66 | 67 | path { 68 | fill: rgba(255, 255, 255, 0.2); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/styles/components/section.scss: -------------------------------------------------------------------------------- 1 | section { 2 | margin-bottom: 25px; 3 | 4 | @include media-breakpoint-up(md) { 5 | padding-bottom: 25px; 6 | border-bottom: solid 1px rgba(white, 0.1); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/components/spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | svg { 12 | width: 50px; 13 | height: 50px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/styles/globals/body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | // background: red; 3 | max-width: 100vw; 4 | overflow: hidden; 5 | color: $black; 6 | background: linear-gradient( 7 | 135deg, 8 | rgba(51, 59, 58, 1) 0%, 9 | rgba(55, 65, 65, 1) 25%, 10 | rgba(64, 54, 43, 1) 75%, 11 | rgba(33, 26, 23, 1) 100% 12 | ); 13 | background-repeat: no-repeat; 14 | background-size: cover; 15 | background-attachment: fixed; 16 | color: white; 17 | -webkit-font-smoothing: antialiased; 18 | min-height: 100vh; 19 | max-height: 100vh; 20 | } 21 | 22 | html, 23 | body { 24 | @include media-breakpoint-down(md) { 25 | // position: fixed; 26 | overflow: hidden; 27 | pointer-events: none; 28 | height: 100vh; 29 | // min-height: calc(100% + env(safe-area-inset-top)); 30 | } 31 | } 32 | 33 | // body { 34 | // @include media-breakpoint-down(md) { 35 | // position: relative; 36 | // } 37 | // } 38 | 39 | #root { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | right: 0; 44 | bottom: 0; 45 | overflow-y: auto; 46 | height: 100%; 47 | } 48 | 49 | .media-backdrop .lazy-load-image-background.blur.lazy-load-image-loaded > img { 50 | opacity: 0.3; 51 | } 52 | 53 | .desktop-only { 54 | @include media-breakpoint-down(md) { 55 | display: none; 56 | } 57 | } 58 | 59 | .align-center { 60 | text-align: center; 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/styles/globals/margins.scss: -------------------------------------------------------------------------------- 1 | $margin: 10px; 2 | 3 | .m { 4 | &b { 5 | &--1 { 6 | margin-bottom: #{$margin * 1}; 7 | } 8 | 9 | &--2 { 10 | margin-bottom: #{$margin * 2}; 11 | } 12 | } 13 | 14 | &t { 15 | &--1 { 16 | margin-top: #{$margin * 1}; 17 | } 18 | 19 | &--2 { 20 | margin-top: #{$margin * 2}; 21 | } 22 | } 23 | 24 | &r { 25 | &--1 { 26 | margin-right: #{$margin * 1}; 27 | } 28 | 29 | &--2 { 30 | margin-right: #{$margin * 2}; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/styles/globals/page.scss: -------------------------------------------------------------------------------- 1 | .page-wrap { 2 | padding: 80px 20px 20px 20px; 3 | max-width: 100vw; 4 | overflow-x: hidden; 5 | min-height: 100vh; 6 | max-height: 100vh; 7 | position: relative; 8 | overflow-y: auto; 9 | // padding-bottom: 120px; 10 | pointer-events: all; 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | width: 100%; 15 | padding-top: calc(80px + env(safe-area-inset-top)); 16 | // transform: translate3d(0, 0, 0); 17 | 18 | &::-webkit-scrollbar { 19 | display: none; 20 | } 21 | 22 | @include media-breakpoint-up(lg) { 23 | position: static; 24 | padding: 50px; 25 | // max-height: calc(100vh - 88px); 26 | max-height: 100vh; 27 | overflow-y: auto; 28 | } 29 | } 30 | 31 | .page { 32 | display: flex; 33 | flex-wrap: nowrap; 34 | 35 | .sidebar { 36 | width: 100%; 37 | 38 | @include media-breakpoint-up(lg) { 39 | width: 250px; 40 | } 41 | } 42 | 43 | .page-wrap { 44 | width: 100%; 45 | @include media-breakpoint-up(lg) { 46 | width: calc(100% - 250px); 47 | } 48 | // margin-top: 88px; 49 | } 50 | } 51 | 52 | .generic-wrap { 53 | @include media-breakpoint-down(md) { 54 | padding-bottom: 120px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/styles/globals/type.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", sans-serif; 3 | font-family: "Lato", sans-serif; 4 | } 5 | 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6, 12 | p, 13 | a { 14 | margin: 0; 15 | text-decoration: none; 16 | color: $black; 17 | user-select: none; 18 | } 19 | 20 | .main-title { 21 | font-size: 24px; 22 | line-height: 28px; 23 | font-weight: 700; 24 | margin-bottom: 0; 25 | color: white; 26 | } 27 | 28 | .single-title { 29 | font-size: 24px; 30 | line-height: 28px; 31 | font-weight: 700; 32 | margin-bottom: 0; 33 | color: white; 34 | 35 | @include media-breakpoint-up(lg) { 36 | font-size: 44px; 37 | line-height: 48px; 38 | } 39 | } 40 | 41 | .sub-title { 42 | font-size: 12px; 43 | line-height: 16px; 44 | font-weight: 700; 45 | letter-spacing: 0; 46 | margin-bottom: 0; 47 | text-transform: uppercase; 48 | color: white; 49 | 50 | @include media-breakpoint-up(lg) { 51 | font-size: 15px; 52 | line-height: 24px; 53 | } 54 | } 55 | 56 | .small { 57 | font-size: 12px; 58 | line-height: 16px; 59 | } 60 | 61 | .upper { 62 | text-transform: uppercase; 63 | } 64 | 65 | p { 66 | margin-bottom: 10px; 67 | font-weight: 300; 68 | color: white; 69 | font-size: 12px; 70 | line-height: 18px; 71 | 72 | @include media-breakpoint-up(lg) { 73 | font-size: 16px; 74 | line-height: 22px; 75 | margin-bottom: 20px; 76 | } 77 | } 78 | 79 | .color { 80 | &-green { 81 | color: $good; 82 | } 83 | 84 | &-orange { 85 | color: $primary; 86 | } 87 | 88 | &-blue { 89 | color: $blue; 90 | } 91 | 92 | &-red { 93 | color: $bad; 94 | } 95 | } 96 | 97 | .capped-width { 98 | width: 500px; 99 | max-width: 100%; 100 | 101 | &__wide { 102 | width: 800px; 103 | max-width: 100%; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | $black: #2a2c2e; 2 | $grey: #f2f2f2; 3 | $grey-light: #e0e3e6; 4 | $grey-medium: #868c96; 5 | $dark-grey: #343536; 6 | $bad: #e95151; 7 | $primary: #d79b23; 8 | $primaryDark: #c78c0d; 9 | $green: #98dd32; 10 | $good: $green; 11 | $blue: #3d85b8; 12 | $purple: #6810af; 13 | $teal: #1f7979; 14 | 15 | // breakpoints ///////////////////////////////////////////// 16 | 17 | $xs: 0; 18 | $sm: 350px; 19 | $md: 768px; 20 | $lg: 1100px; 21 | $xl: 1200px; 22 | $xxl: 1440px; 23 | $xxxl: 1920px; 24 | 25 | $breakpoints: ( 26 | xs: $xs, 27 | sm: $sm, 28 | md: $md, 29 | lg: $lg, 30 | xl: $xl, 31 | xxl: $xxl, 32 | xxxl: $xxxl, 33 | ); 34 | 35 | $container-max-widths: ( 36 | sm: 100%, 37 | md: 100%, 38 | lg: calc(100% - 30px), 39 | xl: calc(100% - 100px), 40 | xxl: calc(100% - 100px), 41 | xxxl: calc(100% - 100px), 42 | ); 43 | 44 | $grid-breakpoints: $breakpoints; 45 | 46 | // dependencies 47 | @import "pre/normalize"; 48 | @import "pre/mixins"; 49 | @import "pre/fonts"; 50 | 51 | // globals 52 | @import "globals/type"; 53 | @import "globals/margins"; 54 | @import "globals/body"; 55 | @import "globals/page"; 56 | @import "globals/sidebar"; 57 | 58 | // components 59 | @import "components/messages"; 60 | @import "components/spinner"; 61 | @import "components/section"; 62 | @import "components/buttons"; 63 | @import "components/config-setup"; 64 | @import "components/card"; 65 | @import "components/carousel"; 66 | @import "components/search-form"; 67 | @import "components/issues"; 68 | @import "components/review"; 69 | @import "components/input"; 70 | @import "components/table"; 71 | @import "components/calendar"; 72 | @import "components/my-requests"; 73 | @import "components/push-msg"; 74 | 75 | // pages 76 | @import "pages/login"; 77 | @import "pages/media"; 78 | @import "pages/season"; 79 | @import "pages/person"; 80 | @import "pages/profile"; 81 | @import "pages/genre"; 82 | @import "pages/networks"; 83 | @import "pages/companies"; 84 | -------------------------------------------------------------------------------- /frontend/src/styles/pages/genre.scss: -------------------------------------------------------------------------------- 1 | .genre { 2 | &--title { 3 | display: flex; 4 | align-items: center; 5 | } 6 | 7 | &--icon { 8 | margin-right: 10px; 9 | display: flex; 10 | 11 | svg { 12 | height: 30px; 13 | margin-right: 5px; 14 | } 15 | } 16 | 17 | &--grid { 18 | display: flex; 19 | flex-wrap: wrap; 20 | margin-left: -5px; 21 | margin-right: -5px; 22 | 23 | &--card { 24 | width: calc(100% / 4); 25 | padding: 0 5px 5px; 26 | 27 | @include media-breakpoint-up(md) { 28 | width: calc(100% / 5); 29 | } 30 | 31 | @include media-breakpoint-up(lg) { 32 | width: calc(100% / 5); 33 | padding: 0 10px 10px; 34 | } 35 | 36 | @include media-breakpoint-up(xl) { 37 | width: calc(100% / 6); 38 | } 39 | 40 | @include media-breakpoint-up(xxl) { 41 | width: calc(100% / 7); 42 | } 43 | 44 | @include media-breakpoint-up(xxxl) { 45 | width: calc(100% / 8); 46 | } 47 | } 48 | 49 | .card { 50 | margin: 0; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/styles/pages/networks.scss: -------------------------------------------------------------------------------- 1 | .network { 2 | &--title { 3 | display: flex; 4 | align-items: center; 5 | 6 | img { 7 | max-height: 50px; 8 | filter: grayscale(1) invert(1); 9 | max-width: 150px; 10 | 11 | @include media-breakpoint-up(lg) { 12 | max-height: 100px; 13 | max-width: 300px; 14 | } 15 | } 16 | } 17 | 18 | &--grid { 19 | display: flex; 20 | flex-wrap: wrap; 21 | margin-left: -5px; 22 | margin-right: -5px; 23 | 24 | &--card { 25 | width: calc(100% / 4); 26 | padding: 0 5px 5px; 27 | 28 | @include media-breakpoint-up(md) { 29 | width: calc(100% / 5); 30 | } 31 | 32 | @include media-breakpoint-up(lg) { 33 | width: calc(100% / 5); 34 | padding: 0 10px 10px; 35 | } 36 | 37 | @include media-breakpoint-up(xl) { 38 | width: calc(100% / 6); 39 | } 40 | 41 | @include media-breakpoint-up(xxl) { 42 | width: calc(100% / 7); 43 | } 44 | 45 | @include media-breakpoint-up(xxxl) { 46 | width: calc(100% / 8); 47 | } 48 | } 49 | 50 | .card { 51 | margin: 0; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petio", 3 | "version": "0.5.7-alpha", 4 | "description": "Request Platform for Plex", 5 | "main": "petio.js", 6 | "author": "", 7 | "license": "ISC", 8 | "dependencies": { 9 | "cluster": "^0.7.7", 10 | "express": "^4.17.1", 11 | "express-http-proxy": "1.6.2", 12 | "http-proxy-middleware": "^1.3.1", 13 | "open": "^8.2.0", 14 | "url": "^0.11.0" 15 | }, 16 | "scripts": { 17 | "start": "node petio.js", 18 | "version": "npm -s run env echo '$npm_package_version'", 19 | "stamp-version": "mv petio.zip petio-${npm_package_version}.zip" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express(); 3 | const path = require("path"); 4 | const { createProxyMiddleware } = require("http-proxy-middleware"); 5 | const logger = require("./api/util/logger"); 6 | 7 | const adminPath = process.pkg 8 | ? path.join(path.dirname(process.execPath), "./views/admin") 9 | : path.join(__dirname, "./views/admin"); 10 | logger.log("verbose", `ROUTER: Serving admin route to ${adminPath}`); 11 | router.use("/admin/", express.static(adminPath)); 12 | 13 | const fePath = process.pkg 14 | ? path.join(path.dirname(process.execPath), "./views/frontend") 15 | : path.join(__dirname, "./views/frontend"); 16 | logger.log("verbose", `ROUTER: Serving frontend route to ${fePath}`); 17 | router.use("/", express.static(fePath)); 18 | 19 | router.use( 20 | "/api", 21 | createProxyMiddleware({ 22 | target: "http://localhost:7778", 23 | headers: { 24 | Connection: "keep-alive", 25 | }, 26 | xfwd: true, 27 | logProvider: function (provider) { 28 | return logger; 29 | }, 30 | pathRewrite: function (path, req) { 31 | if (req.basePath !== "/") { 32 | return path.replace("//", "/").replace(`${req.basePath}/api/`, "/"); 33 | } else { 34 | return path.replace("/api/", "/"); 35 | } 36 | }, 37 | }) 38 | ); 39 | logger.log("verbose", `ROUTER: API proxy setup - Proxying /api -> /`); 40 | 41 | router.get("*", function (req, res) { 42 | logger.log("warn", `ROUTER: Not found - ${req.path} | IP: ${req.ip}`); 43 | res.status(404).send(`Petio Router: not found - ${req.path}`); 44 | }); 45 | 46 | module.exports = router; 47 | --------------------------------------------------------------------------------