├── .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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
15 |
23 |
31 |
39 |
47 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------