├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-app.yml │ ├── build-server.yml │ ├── lint.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── .mocharc.js ├── .prettierrc.js ├── Dockerfile ├── README.md ├── app ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.vue │ ├── apollo.ts │ ├── components │ │ ├── ActorGrid.vue │ │ ├── ActorSelector.vue │ │ ├── AppBar │ │ │ ├── ActorDetails.vue │ │ │ ├── MovieDetails.vue │ │ │ ├── SceneDetails.vue │ │ │ └── StudioDetails.vue │ │ ├── BindTitle.vue │ │ ├── Cards │ │ │ └── Actor.vue │ │ ├── Collabs.vue │ │ ├── CreatedCustomField.vue │ │ ├── CustomFieldCreator.vue │ │ ├── CustomFieldFilter.vue │ │ ├── CustomFieldSelector.vue │ │ ├── DVDRenderer.vue │ │ ├── DateInput.vue │ │ ├── Divider.vue │ │ ├── Flag.vue │ │ ├── HomeWidgets │ │ │ ├── ActorLabelUsage.vue │ │ │ ├── Base.vue │ │ │ ├── QueueInfo.vue │ │ │ ├── RemainingTime.vue │ │ │ ├── Scan.vue │ │ │ ├── SceneLabelUsage.vue │ │ │ ├── SearchTimes.vue │ │ │ ├── Stats.vue │ │ │ ├── TopActors.vue │ │ │ └── UnwatchedActors.vue │ │ ├── ImageCard.vue │ │ ├── ImageUploader.vue │ │ ├── LabelFilter.vue │ │ ├── LabelSelector.vue │ │ ├── Lightbox.vue │ │ ├── Loading.vue │ │ ├── MarkerCard.vue │ │ ├── MarkerItem.vue │ │ ├── MovieCard.vue │ │ ├── MovieItem.vue │ │ ├── NoResults.vue │ │ ├── Plugins │ │ │ └── Item.vue │ │ ├── Rating.vue │ │ ├── SceneCard.vue │ │ ├── SceneSelector.vue │ │ ├── SceneUploader.vue │ │ ├── StudioCard.vue │ │ ├── StudioSelector.vue │ │ └── VideoPlayer.vue │ ├── fragments │ │ ├── actor.ts │ │ ├── image.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ ├── main.ts │ ├── mixins │ │ ├── drawer.ts │ │ └── scene.ts │ ├── plugins │ │ └── vuetify.ts │ ├── router │ │ └── index.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── actor.ts │ │ ├── context.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── markers.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ ├── types │ │ ├── actor.ts │ │ ├── image.ts │ │ ├── label.ts │ │ ├── movie.ts │ │ └── scene.ts │ ├── util │ │ ├── color.ts │ │ ├── countries.ts │ │ ├── object.ts │ │ └── scene.ts │ └── views │ │ ├── About.vue │ │ ├── ActorDetails.vue │ │ ├── Actors.vue │ │ ├── Graph.vue │ │ ├── Home.vue │ │ ├── Images.vue │ │ ├── Labels.vue │ │ ├── Logs.vue │ │ ├── Markers.vue │ │ ├── MovieDetails.vue │ │ ├── Movies.vue │ │ ├── Plugins.vue │ │ ├── SceneDetails.vue │ │ ├── Scenes.vue │ │ ├── StudioDetails.vue │ │ ├── Studios.vue │ │ └── Views.vue ├── tsconfig.json └── vue.config.js ├── assets ├── OrbitControls.js ├── bump.jpg ├── favicon.png ├── flags │ ├── ad.svg │ ├── ae.svg │ ├── af.svg │ ├── ag.svg │ ├── ai.svg │ ├── al.svg │ ├── am.svg │ ├── ao.svg │ ├── aq.svg │ ├── ar.svg │ ├── as.svg │ ├── at.svg │ ├── au.svg │ ├── aw.svg │ ├── ax.svg │ ├── az.svg │ ├── ba.svg │ ├── bb.svg │ ├── bd.svg │ ├── be.svg │ ├── bf.svg │ ├── bg.svg │ ├── bh.svg │ ├── bi.svg │ ├── bj.svg │ ├── bl.svg │ ├── bm.svg │ ├── bn.svg │ ├── bo.svg │ ├── bq.svg │ ├── br.svg │ ├── bs.svg │ ├── bt.svg │ ├── bv.svg │ ├── bw.svg │ ├── by.svg │ ├── bz.svg │ ├── ca.svg │ ├── cc.svg │ ├── cd.svg │ ├── cf.svg │ ├── cg.svg │ ├── ch.svg │ ├── ci.svg │ ├── ck.svg │ ├── cl.svg │ ├── cm.svg │ ├── cn.svg │ ├── co.svg │ ├── cr.svg │ ├── cu.svg │ ├── cv.svg │ ├── cw.svg │ ├── cx.svg │ ├── cy.svg │ ├── cz.svg │ ├── de.svg │ ├── dj.svg │ ├── dk.svg │ ├── dm.svg │ ├── do.svg │ ├── dz.svg │ ├── ec.svg │ ├── ee.svg │ ├── eg.svg │ ├── eh.svg │ ├── er.svg │ ├── es-ct.svg │ ├── es.svg │ ├── et.svg │ ├── eu.svg │ ├── fi.svg │ ├── fj.svg │ ├── fk.svg │ ├── fm.svg │ ├── fo.svg │ ├── fr.svg │ ├── ga.svg │ ├── gb-eng.svg │ ├── gb-nir.svg │ ├── gb-sct.svg │ ├── gb-wls.svg │ ├── gb.svg │ ├── gd.svg │ ├── ge.svg │ ├── gf.svg │ ├── gg.svg │ ├── gh.svg │ ├── gi.svg │ ├── gl.svg │ ├── gm.svg │ ├── gn.svg │ ├── gp.svg │ ├── gq.svg │ ├── gr.svg │ ├── gs.svg │ ├── gt.svg │ ├── gu.svg │ ├── gw.svg │ ├── gy.svg │ ├── hk.svg │ ├── hm.svg │ ├── hn.svg │ ├── hr.svg │ ├── ht.svg │ ├── hu.svg │ ├── id.svg │ ├── ie.svg │ ├── il.svg │ ├── im.svg │ ├── in.svg │ ├── io.svg │ ├── iq.svg │ ├── ir.svg │ ├── is.svg │ ├── it.svg │ ├── je.svg │ ├── jm.svg │ ├── jo.svg │ ├── jp.svg │ ├── ke.svg │ ├── kg.svg │ ├── kh.svg │ ├── ki.svg │ ├── km.svg │ ├── kn.svg │ ├── kp.svg │ ├── kr.svg │ ├── kw.svg │ ├── ky.svg │ ├── kz.svg │ ├── la.svg │ ├── lb.svg │ ├── lc.svg │ ├── li.svg │ ├── lk.svg │ ├── lr.svg │ ├── ls.svg │ ├── lt.svg │ ├── lu.svg │ ├── lv.svg │ ├── ly.svg │ ├── ma.svg │ ├── mc.svg │ ├── md.svg │ ├── me.svg │ ├── mf.svg │ ├── mg.svg │ ├── mh.svg │ ├── mk.svg │ ├── ml.svg │ ├── mm.svg │ ├── mn.svg │ ├── mo.svg │ ├── mp.svg │ ├── mq.svg │ ├── mr.svg │ ├── ms.svg │ ├── mt.svg │ ├── mu.svg │ ├── mv.svg │ ├── mw.svg │ ├── mx.svg │ ├── my.svg │ ├── mz.svg │ ├── na.svg │ ├── nc.svg │ ├── ne.svg │ ├── nf.svg │ ├── ng.svg │ ├── ni.svg │ ├── nl.svg │ ├── no.svg │ ├── np.svg │ ├── nr.svg │ ├── nu.svg │ ├── nz.svg │ ├── om.svg │ ├── pa.svg │ ├── pe.svg │ ├── pf.svg │ ├── pg.svg │ ├── ph.svg │ ├── pk.svg │ ├── pl.svg │ ├── pm.svg │ ├── pn.svg │ ├── pr.svg │ ├── ps.svg │ ├── pt.svg │ ├── pw.svg │ ├── py.svg │ ├── qa.svg │ ├── re.svg │ ├── ro.svg │ ├── rs.svg │ ├── ru.svg │ ├── rw.svg │ ├── sa.svg │ ├── sb.svg │ ├── sc.svg │ ├── sd.svg │ ├── se.svg │ ├── sg.svg │ ├── sh.svg │ ├── si.svg │ ├── sj.svg │ ├── sk.svg │ ├── sl.svg │ ├── sm.svg │ ├── sn.svg │ ├── so.svg │ ├── sr.svg │ ├── ss.svg │ ├── st.svg │ ├── sv.svg │ ├── sx.svg │ ├── sy.svg │ ├── sz.svg │ ├── tc.svg │ ├── td.svg │ ├── tf.svg │ ├── tg.svg │ ├── th.svg │ ├── tj.svg │ ├── tk.svg │ ├── tl.svg │ ├── tm.svg │ ├── tn.svg │ ├── to.svg │ ├── tr.svg │ ├── tt.svg │ ├── tv.svg │ ├── tw.svg │ ├── tz.svg │ ├── ua.svg │ ├── ug.svg │ ├── um.svg │ ├── un.svg │ ├── us.svg │ ├── uy.svg │ ├── uz.svg │ ├── va.svg │ ├── vc.svg │ ├── ve.svg │ ├── vg.svg │ ├── vi.svg │ ├── vn.svg │ ├── vu.svg │ ├── wf.svg │ ├── ws.svg │ ├── ye.svg │ ├── yt.svg │ ├── za.svg │ ├── zm.svg │ └── zw.svg └── three.min.js ├── config.json.example ├── doc ├── build_from_source.md ├── config.md ├── docker.md ├── guides.md ├── img │ ├── actor_collection.jpg │ ├── actor_details.jpg │ ├── btc.png │ ├── image_collection.jpg │ ├── image_details.jpg │ ├── movie_collection.jpg │ ├── movie_details.jpg │ ├── parent_studio.jpg │ ├── plugin_filter.png │ ├── scene_collection.jpg │ ├── scene_details.jpg │ └── studio_collection.jpg ├── import.md ├── pipe_plugins.md ├── plugin_development.md └── plugins_intro.md ├── nodemon.json ├── package-lock.json ├── package.json ├── run.sh ├── src ├── apollo.ts ├── args.ts ├── backup.ts ├── broken_image.ts ├── config │ ├── default.ts │ ├── index.ts │ ├── schema.ts │ └── validate.ts ├── data │ └── countries.ts ├── database │ ├── index.ts │ └── internal │ │ └── index.ts ├── dvd_renderer.ts ├── exit.ts ├── extractor.ts ├── ffmpeg-download.ts ├── ffmpeg │ └── screenshot.ts ├── fs │ ├── async.ts │ └── index.ts ├── gianna.ts ├── graphql │ ├── mutation.ts │ ├── mutations │ │ ├── actor.ts │ │ ├── custom_field.ts │ │ ├── image.ts │ │ ├── label.ts │ │ ├── marker.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ ├── resolvers.ts │ ├── resolvers │ │ ├── actor.ts │ │ ├── custom_field.ts │ │ ├── image.ts │ │ ├── label.ts │ │ ├── marker.ts │ │ ├── movie.ts │ │ ├── query.ts │ │ ├── scene.ts │ │ ├── scene_view.ts │ │ ├── search │ │ │ ├── actor.ts │ │ │ ├── image.ts │ │ │ ├── marker.ts │ │ │ ├── movie.ts │ │ │ ├── scene.ts │ │ │ └── studio.ts │ │ └── studio.ts │ ├── schema │ │ ├── actor.ts │ │ ├── custom_field.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── label.ts │ │ ├── marker.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ └── types.ts ├── hash.ts ├── import │ ├── create.ts │ ├── index.ts │ ├── schemas │ │ ├── actor.ts │ │ ├── common.ts │ │ ├── custom_field.ts │ │ ├── label.ts │ │ ├── marker.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ ├── types.ts │ ├── validate.ts │ └── verify.ts ├── index.ts ├── izzy.ts ├── logger.ts ├── mem.ts ├── middlewares │ └── cors.ts ├── password.ts ├── plugin_events │ ├── actor.ts │ ├── movie.ts │ └── scene.ts ├── plugins │ ├── index.ts │ └── validate.ts ├── query_extractor.ts ├── queue │ ├── check.ts │ └── processing.ts ├── queue_loop.ts ├── queue_router.ts ├── render.ts ├── scanner.ts ├── search │ ├── actor.ts │ ├── common.ts │ ├── image.ts │ ├── index.ts │ ├── internal │ │ └── index.ts │ ├── marker.ts │ ├── movie.ts │ ├── scene.ts │ └── studio.ts ├── server.ts ├── setup.ts ├── types │ ├── actor.ts │ ├── actor_reference.ts │ ├── countries.ts │ ├── custom_field.ts │ ├── image.ts │ ├── label.ts │ ├── labelled_item.ts │ ├── marker.ts │ ├── movie.ts │ ├── movie_scene.ts │ ├── scene.ts │ ├── studio.ts │ ├── utility.ts │ └── watch.ts └── typings │ ├── js-sha512.d.ts │ └── merge-img.d.ts ├── test ├── config │ ├── index.fixture.ts │ ├── index.spec.ts │ ├── schema.fixture.ts │ └── schema.spec.ts ├── fixtures │ ├── files │ │ ├── image001.jpg │ │ ├── image002.jpg │ │ ├── image003.jpg │ │ ├── image004.jpg │ │ ├── image005.jpg │ │ ├── some_folder │ │ │ ├── image006.jpg │ │ │ ├── image007.jpg │ │ │ ├── image008.jpg │ │ │ ├── image009.jpg │ │ │ └── image010.jpg │ │ └── video001.mp4 │ ├── matching_actor.fixture.ts │ ├── matching_label.fixture.ts │ ├── remove_extension.fixture.ts │ ├── strip_string.fixture.ts │ ├── url_ext.fixture.ts │ └── walk.fixture.ts ├── init.ts ├── string.spec.ts ├── url_ext.spec.ts ├── util.ts └── walk.spec.ts ├── tsconfig.dev.json ├── tsconfig.json └── views ├── dvd-renderer.html ├── error.html ├── setup.html └── signin.html /.eslintignore: -------------------------------------------------------------------------------- 1 | # will probably be deprecated after 0.23 anyway 2 | src/compat/index.ts 3 | # TODO: do later 4 | src/import/**/*.ts 5 | dvd_renderer.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "standard", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "prettier/@typescript-eslint", 12 | "plugin:prettier/recommended", 13 | ], 14 | globals: { 15 | Atomics: "readonly", 16 | SharedArrayBuffer: "readonly", 17 | }, 18 | parser: "@typescript-eslint/parser", 19 | parserOptions: { 20 | ecmaVersion: 11, 21 | project: ["./tsconfig.json"], 22 | sourceType: "module", 23 | tsconfigRootDir: __dirname, 24 | }, 25 | plugins: ["@typescript-eslint", "simple-import-sort"], 26 | root: true, 27 | rules: { 28 | "@typescript-eslint/restrict-template-expressions": "off", 29 | "@typescript-eslint/no-misused-promises": "off", 30 | "no-async-promise-executor": "off", 31 | "@typescript-eslint/no-namespace": "off", 32 | // Turn off default import rules 33 | "sort-imports": "off", 34 | "import/order": "off", 35 | // Use simple-import-sort instead 36 | "simple-import-sort/sort": "error", 37 | "@typescript-eslint/unbound-method": [ 38 | "error", 39 | { 40 | ignoreStatic: true, 41 | }, 42 | ], 43 | "@typescript-eslint/restrict-template-expressions": [ 44 | "error", 45 | { 46 | allowNumber: true, 47 | allowBoolean: true, 48 | allowNullish: true, 49 | }, 50 | ], 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 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: "[Feature] <Title>" 5 | labels: new feature 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 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/build-app.yml: -------------------------------------------------------------------------------- 1 | name: Build app 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - dev 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [8.x] 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Test app 24 | run: | 25 | npm run install:app 26 | npm run build:app 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.github/workflows/build-server.yml: -------------------------------------------------------------------------------- 1 | name: Build server 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - dev 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [8.x] 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Test server 24 | run: | 25 | npm install 26 | npm run transpile 27 | npm run test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | on: [pull_request] 3 | jobs: 4 | eslint-added: 5 | name: runner / eslint-added 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: eslint 10 | uses: reviewdog/action-eslint@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | reporter: github-check 14 | eslint_flags: "src/**/*.ts" 15 | fail_on_error: true 16 | filter_mode: added 17 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push dev 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Discord notification 14 | env: 15 | DISCORD_USERNAME: GitHub 16 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_HOOK }} 17 | uses: Ilshidur/action-discord@master 18 | with: 19 | args: 'New commit to *dev* by **{{ EVENT_PAYLOAD.pusher.name }}**' 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '10.x' 15 | - name: Install 16 | run: | 17 | npm ci 18 | npm run install:app 19 | - name: Build 20 | run: | 21 | npm run build:all 22 | - name: Release 23 | uses: softprops/action-gh-release@v1 24 | if: startsWith(github.ref, 'refs/tags/') 25 | with: 26 | files: | 27 | releases/mac.zip 28 | releases/linux.zip 29 | releases/win.zip 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | - name: Discord notification 33 | env: 34 | DISCORD_USERNAME: GitHub 35 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_HOOK }} 36 | uses: Ilshidur/action-discord@master 37 | with: 38 | args: 'Version **{{ EVENT_PAYLOAD.release.tag_name }}** released!!' 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Dependency directory 24 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 25 | node_modules 26 | build/ 27 | db\.json 28 | 29 | 30 | config\.json 31 | 32 | library/* 33 | bin/ 34 | 35 | configg\.json 36 | /ffmpeg 37 | /ffprobe 38 | app/dist/ 39 | release/ 40 | releases/ 41 | tmp/ 42 | backups/ 43 | /plugins/ 44 | imports/ 45 | 46 | config\.yaml 47 | 48 | images\.json 49 | 50 | izzy 51 | izzy.exe 52 | compat/cross_references\.db 53 | 54 | .vscode 55 | 56 | config.test.json 57 | config.test.yaml 58 | 59 | gianna 60 | gianna.exe 61 | ffmpeg.exe 62 | ffprobe.exe 63 | 64 | config\.old\.yaml 65 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | process.env.PREVENT_STARTUP = "true"; 2 | process.env.NODE_ENV = "test"; 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "es5", 4 | singleQuote: false, 5 | printWidth: 100, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-buster as build-env 2 | WORKDIR /app 3 | ADD . /app 4 | RUN cd /app && \ 5 | npm install && \ 6 | npm run install:app && \ 7 | cd /app && \ 8 | npm run build 9 | 10 | from debian:buster 11 | COPY --from=build-env /app/release/porn-vault / 12 | COPY --from=build-env /app/release/app/ /app 13 | COPY --from=build-env /app/release/views/ /views 14 | RUN apt-get update && apt-get -y install ca-certificates ffmpeg && rm -rf /var/lib/apt/lists/* 15 | 16 | COPY assets /assets 17 | 18 | COPY config.json.example / 19 | COPY run.sh / 20 | VOLUME [ "/config" ] 21 | EXPOSE 3000 22 | ENTRYPOINT ["/run.sh"] 23 | -------------------------------------------------------------------------------- /app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 | <meta name="viewport" content="width=device-width,initial-scale=1.0" /> 7 | <link rel="icon" href="/assets/favicon.png" /> 8 | <title>app 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/apollo.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from "apollo-client"; 2 | import { InMemoryCache } from "apollo-cache-inmemory"; 3 | import { setContext } from "apollo-link-context"; 4 | import { createUploadLink } from "apollo-upload-client"; 5 | 6 | const port = 3000; // Server port set in config 7 | 8 | export const serverBase = 9 | process.env.NODE_ENV == "development" ? `http://localhost:${port}` : ""; 10 | 11 | const authLink = setContext((_, { headers }) => { 12 | return { 13 | headers: { 14 | ...headers, 15 | "X-PASS": localStorage.getItem("password"), 16 | }, 17 | }; 18 | }); 19 | 20 | export default new ApolloClient({ 21 | link: authLink.concat(createUploadLink({ uri: `${serverBase}/ql` })), 22 | cache: new InMemoryCache(), 23 | defaultOptions: { 24 | watchQuery: { fetchPolicy: "no-cache", errorPolicy: "ignore" }, 25 | query: { fetchPolicy: "no-cache", errorPolicy: "all" }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /app/src/components/BindTitle.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /app/src/components/Divider.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/components/Flag.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/components/HomeWidgets/Base.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/components/HomeWidgets/QueueInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/components/HomeWidgets/TopActors.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/components/HomeWidgets/UnwatchedActors.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/components/ImageCard.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/components/NoResults.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/fragments/actor.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | fragment ActorFragment on Actor { 5 | _id 6 | name 7 | description 8 | bornOn 9 | age 10 | aliases 11 | rating 12 | favorite 13 | bookmark 14 | customFields 15 | availableFields { 16 | _id 17 | name 18 | type 19 | values 20 | unit 21 | } 22 | nationality { 23 | name 24 | alpha2 25 | nationality 26 | } 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /app/src/fragments/image.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | fragment ImageFragment on Image { 5 | _id 6 | name 7 | bookmark 8 | favorite 9 | rating 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /app/src/fragments/movie.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | fragment MovieFragment on Movie { 5 | _id 6 | name 7 | releaseDate 8 | description 9 | rating 10 | favorite 11 | bookmark 12 | labels { 13 | _id 14 | name 15 | } 16 | frontCover { 17 | _id 18 | color 19 | } 20 | backCover { 21 | _id 22 | } 23 | spineCover { 24 | _id 25 | } 26 | studio { 27 | _id 28 | name 29 | } 30 | duration 31 | size 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /app/src/fragments/scene.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | fragment SceneFragment on Scene { 5 | _id 6 | addedOn 7 | name 8 | releaseDate 9 | description 10 | rating 11 | favorite 12 | bookmark 13 | studio { 14 | _id 15 | name 16 | } 17 | labels { 18 | _id 19 | name 20 | } 21 | thumbnail { 22 | _id 23 | color 24 | } 25 | meta { 26 | size 27 | duration 28 | fps 29 | dimensions { 30 | width 31 | height 32 | } 33 | } 34 | watches 35 | streamLinks 36 | path 37 | customFields 38 | availableFields { 39 | _id 40 | name 41 | type 42 | values 43 | unit 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /app/src/fragments/studio.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | fragment StudioFragment on Studio { 5 | _id 6 | name 7 | description 8 | aliases 9 | rating 10 | favorite 11 | bookmark 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import vuetify from "./plugins/vuetify"; 6 | import "roboto-fontface/css/roboto/roboto-fontface.css"; 7 | import "@mdi/font/css/materialdesignicons.css"; 8 | import VueTheMask from "vue-the-mask"; 9 | import vueScrollBehavior from "vue-scroll-behavior"; 10 | 11 | import BindTitle from "./components/BindTitle.vue"; 12 | import DateInput from "./components/DateInput.vue"; 13 | import Rating from "./components/Rating.vue"; 14 | import LabelFilter from "./components/LabelFilter.vue"; 15 | import Divider from "./components/Divider.vue"; 16 | import NoResults from "./components/NoResults.vue"; 17 | import Loading from "./components/Loading.vue"; 18 | import Flag from "./components/Flag.vue"; 19 | import WidgetCard from "./components/HomeWidgets/Base.vue"; 20 | 21 | Vue.use(vueScrollBehavior, { router: router }); 22 | Vue.use(VueTheMask); 23 | 24 | Vue.component("BindTitle", BindTitle); 25 | Vue.component("DateInput", DateInput); 26 | Vue.component("Rating", Rating); 27 | Vue.component("LabelFilter", LabelFilter); 28 | Vue.component("Divider", Divider); 29 | Vue.component("NoResults", NoResults); 30 | Vue.component("Loading", Loading); 31 | Vue.component("Flag", Flag); 32 | Vue.component("WidgetCard", WidgetCard); 33 | 34 | Vue.config.productionTip = false; 35 | 36 | new Vue({ 37 | router, 38 | store, 39 | vuetify, 40 | render: (h) => h(App), 41 | }).$mount("#app"); 42 | -------------------------------------------------------------------------------- /app/src/mixins/drawer.ts: -------------------------------------------------------------------------------- 1 | // mixin.js 2 | import Vue from "vue"; 3 | import Component from "vue-class-component"; 4 | import { contextModule } from "../store/context"; 5 | 6 | @Component 7 | export default class DrawerMixin extends Vue { 8 | get drawer() { 9 | return this.$vuetify.breakpoint.lgAndUp || contextModule.showFilters; 10 | } 11 | 12 | set drawer(val: boolean) { 13 | contextModule.toggleFilters(val); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify/lib"; 3 | import colors from "vuetify/lib/util/colors"; 4 | 5 | Vue.use(Vuetify); 6 | 7 | export default new Vuetify({ 8 | theme: { 9 | options: { 10 | customProperties: true 11 | }, 12 | themes: { 13 | light: { 14 | primary: colors.blue.base, 15 | error: colors.red.accent3, 16 | info: colors.blue.darken2, 17 | success: colors.green.base, 18 | warning: colors.orange.darken2 19 | }, 20 | dark: { 21 | primary: colors.blue.lighten3, 22 | error: colors.red.accent2, 23 | info: colors.blue.darken2, 24 | success: colors.green.base, 25 | warning: colors.orange.lighten2 26 | } 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /app/src/store/context.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation, Action } from "vuex-class-modules"; 2 | 3 | @Module 4 | class ContextModule extends VuexModule { 5 | showFilters = false; 6 | 7 | sceneAspectRatio = 1; 8 | actorAspectRatio = 1; 9 | scenePauseOnUnfocus = false; 10 | showCardLabels = true; 11 | 12 | fillActorCards = true; 13 | 14 | showSidenav = true; // TODO: store and load from localStorage 15 | scenePreviewOnMouseHover = false; 16 | 17 | @Mutation 18 | toggleSidenav(bool: boolean) { 19 | this.showSidenav = bool; 20 | } 21 | 22 | @Mutation 23 | toggleActorCardStyle(bool: boolean) { 24 | this.fillActorCards = bool; 25 | } 26 | 27 | @Mutation 28 | toggleCardLabels(bool: boolean) { 29 | this.showCardLabels = bool; 30 | } 31 | 32 | @Mutation 33 | toggleFilters(bool: boolean) { 34 | this.showFilters = bool; 35 | } 36 | 37 | @Mutation 38 | setScenePauseOnUnfocus(val: boolean) { 39 | this.scenePauseOnUnfocus = val; 40 | } 41 | 42 | @Mutation 43 | setSceneAspectRatio(val: number) { 44 | this.sceneAspectRatio = val; 45 | } 46 | 47 | @Mutation 48 | setActorAspectRatio(val: number) { 49 | this.actorAspectRatio = val; 50 | } 51 | 52 | @Mutation 53 | setScenePreviewOnMouseHover(val: boolean) { 54 | this.scenePreviewOnMouseHover = val; 55 | } 56 | } 57 | 58 | import store from "./index"; 59 | export const contextModule = new ContextModule({ store, name: "context" }); 60 | -------------------------------------------------------------------------------- /app/src/store/image.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation, Action } from "vuex-class-modules"; 2 | 3 | @Module 4 | class ImageModule extends VuexModule { 5 | page = 1; 6 | numResults = 0; 7 | numPages = 0; 8 | // items = [] as IImage[]; 9 | 10 | /* @Mutation 11 | unshift(items: IImage[]) { 12 | this.items.unshift(...items); 13 | } */ 14 | 15 | @Mutation 16 | resetPagination() { 17 | // this.items = []; 18 | this.numPages = 0; 19 | this.numResults = 0; 20 | this.page = 1; 21 | } 22 | 23 | @Mutation 24 | setPage(num: number) { 25 | this.page = num; 26 | } 27 | 28 | /* @Mutation 29 | removeImages(ids: string[]) { 30 | for (const id of ids) { 31 | this.items = this.items.filter((img) => img._id != id); 32 | } 33 | } */ 34 | 35 | @Mutation 36 | setPagination({ 37 | // items, 38 | numResults, 39 | numPages, 40 | }: { 41 | // items: IImage[]; 42 | numResults: number; 43 | numPages: number; 44 | }) { 45 | // this.items = items; 46 | this.numResults = numResults; 47 | this.numPages = numPages; 48 | } 49 | } 50 | 51 | import store from "./index"; 52 | import IImage from "@/types/image"; 53 | export const imageModule = new ImageModule({ store, name: "images" }); 54 | -------------------------------------------------------------------------------- /app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({}) 7 | -------------------------------------------------------------------------------- /app/src/store/markers.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation, Action } from "vuex-class-modules"; 2 | 3 | @Module 4 | class MarkerModule extends VuexModule { 5 | page = 1; 6 | numResults = 0; 7 | numPages = 0; 8 | 9 | @Mutation 10 | resetPagination() { 11 | // this.items = []; 12 | this.numPages = 0; 13 | this.numResults = 0; 14 | this.page = 1; 15 | } 16 | 17 | @Mutation 18 | setPage(num: number) { 19 | this.page = num; 20 | } 21 | 22 | @Mutation 23 | setPagination({ 24 | // items, 25 | numResults, 26 | numPages, 27 | }: { 28 | // items: IMarker[]; 29 | numResults: number; 30 | numPages: number; 31 | }) { 32 | // this.items = items; 33 | this.numResults = numResults; 34 | this.numPages = numPages; 35 | } 36 | } 37 | 38 | import store from "./index"; 39 | export const markerModule = new MarkerModule({ store, name: "markers" }); 40 | -------------------------------------------------------------------------------- /app/src/types/actor.ts: -------------------------------------------------------------------------------- 1 | type AttachedImage = { 2 | _id: string; 3 | } | null; 4 | 5 | export interface ICollabActor { 6 | _id: string; 7 | name: string; 8 | thumbnail: AttachedImage; 9 | avatar: AttachedImage; 10 | } 11 | 12 | export default interface IActor { 13 | _id: string; 14 | name: string; 15 | description: string | null; 16 | bornOn: number | null; 17 | age: number | null; 18 | aliases: string[]; 19 | rating: number | null; 20 | favorite: boolean; 21 | bookmark: number | null; 22 | labels: { 23 | _id: string; 24 | name: string; 25 | }[]; 26 | thumbnail: { 27 | _id: string; 28 | color: string | null; 29 | } | null; 30 | altThumbnail: { 31 | _id: string; 32 | color?: string | null; 33 | } | null; 34 | hero?: { 35 | _id: string; 36 | color?: string | null; 37 | } | null; 38 | avatar?: { 39 | _id: string; 40 | color?: string | null; 41 | } | null; 42 | customFields: { _id: string; name: string; values?: string[]; type: string }; 43 | availableFields: { 44 | _id: string; 45 | name: string; 46 | values?: string[]; 47 | type: string; 48 | unit: string | null; 49 | }[]; 50 | nationality: { 51 | name: string; 52 | alpha2: string; 53 | nationality: string; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /app/src/types/image.ts: -------------------------------------------------------------------------------- 1 | import IActor from "./actor"; 2 | 3 | export default interface IImage { 4 | _id: string; 5 | name: string; 6 | labels: { 7 | _id: string; 8 | name: string; 9 | }[]; 10 | scene: { 11 | _id: string; 12 | name: string; 13 | }; 14 | actors: IActor[]; 15 | bookmark: boolean; 16 | favorite: boolean; 17 | rating: number | null; 18 | color?: string | null; 19 | } 20 | -------------------------------------------------------------------------------- /app/src/types/label.ts: -------------------------------------------------------------------------------- 1 | export default interface ILabel { 2 | _id: string; 3 | name: string; 4 | aliases: string[]; 5 | thumbnail: { 6 | _id: string; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/types/movie.ts: -------------------------------------------------------------------------------- 1 | import IActor from "./actor"; 2 | import IScene from "./scene"; 3 | 4 | export default interface IMovie { 5 | _id: string; 6 | name: string; 7 | description: string | null; 8 | releaseDate: number | null; 9 | rating: number | null; 10 | favorite: boolean; 11 | bookmark: number | null; 12 | 13 | frontCover: { 14 | _id: string; 15 | color: string | null; 16 | } | null; 17 | backCover: { 18 | _id: string; 19 | } | null; 20 | spineCover: { 21 | _id: string; 22 | } | null; 23 | studio: any; 24 | scenes: IScene[]; 25 | actors: IActor[]; 26 | labels: { 27 | _id: string; 28 | name: string; 29 | }[]; 30 | 31 | duration: number | null; 32 | size: number | null; 33 | } 34 | -------------------------------------------------------------------------------- /app/src/types/scene.ts: -------------------------------------------------------------------------------- 1 | import IActor from "./actor"; 2 | 3 | export default interface IScene { 4 | _id: string; 5 | addedOn: number; 6 | name: string; 7 | releaseDate: number | null; 8 | description: string | null; 9 | rating: number | null; 10 | favorite: boolean; 11 | bookmark: number | null; 12 | actors: IActor[]; 13 | studio: any; 14 | labels: { 15 | _id: string; 16 | name: string; 17 | }[]; 18 | thumbnail: { 19 | _id: string; 20 | color: string | null; 21 | } | null; 22 | preview: { 23 | _id: string; 24 | } | null; 25 | meta: { 26 | size: number; 27 | duration: number; 28 | dimensions: { 29 | width: number; 30 | height: number; 31 | }; 32 | }; 33 | watches: number[]; 34 | streamLinks: string[]; 35 | customFields: { [key: string]: any }; 36 | availableFields: { 37 | _id: string; 38 | name: string; 39 | values?: string[]; 40 | type: string; 41 | unit: string | null; 42 | }[]; 43 | } 44 | -------------------------------------------------------------------------------- /app/src/util/color.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | 3 | export function ensureDarkColor(hex: string) { 4 | const col = Color(hex); 5 | if (col.value() > 50) { 6 | return Color( 7 | [col.hue(), col.saturationv(), col.value() - (col.value() - 30)], 8 | "hsv" 9 | ).hex(); 10 | } 11 | return hex; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/util/object.ts: -------------------------------------------------------------------------------- 1 | export function copy(t: T) { 2 | return JSON.parse(JSON.stringify(t)) as T; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/util/scene.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from "../apollo"; 2 | import gql from "graphql-tag"; 3 | import IScene from "@/types/scene"; 4 | import { sceneModule } from "../store/scene"; 5 | 6 | export function watch(scene: IScene) { 7 | ApolloClient.mutate({ 8 | mutation: gql` 9 | mutation($id: String!) { 10 | watchScene(id: $id) { 11 | watches 12 | } 13 | } 14 | `, 15 | variables: { 16 | id: scene._id 17 | } 18 | }) 19 | .then(res => { 20 | sceneModule.pushWatch(res.data.watchScene.watches.pop()); 21 | }) 22 | .catch(err => { 23 | console.error(err); 24 | }); 25 | } 26 | 27 | export function unwatch(scene: IScene) { 28 | ApolloClient.mutate({ 29 | mutation: gql` 30 | mutation($id: String!) { 31 | unwatchScene(id: $id) { 32 | watches 33 | } 34 | } 35 | `, 36 | variables: { 37 | id: scene._id 38 | } 39 | }) 40 | .then(res => { 41 | sceneModule.popWatch(); 42 | }) 43 | .catch(err => { 44 | console.error(err); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "noImplicitAny": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "vue", 17 | "webpack-env", 18 | "vuetify" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /app/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transpileDependencies": [ 3 | "vuetify" 4 | ] 5 | } -------------------------------------------------------------------------------- /assets/bump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/assets/bump.jpg -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/assets/favicon.png -------------------------------------------------------------------------------- /assets/flags/ae.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/am.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/at.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/au.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/ax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/flags/az.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/ba.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/bb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/bd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/be.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/bf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/bh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/bi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/bj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/bl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/bq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/bs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/bv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/bw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ca.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/cd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/cf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/cg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/ch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/ci.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/cl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/cm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/cn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/co.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/cr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/cu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/cv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/cw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/cz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/de.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/dj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/dk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/dz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/ee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/eh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/es-ct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/et.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/eu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /assets/flags/fi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/fm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/fo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/fr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ga.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/gb-eng.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/gb-sct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/gb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/ge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/gf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/gg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/gh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/gl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/gm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/gn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/gp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/gr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /assets/flags/gw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/gy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/hm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/hn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/flags/hu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/il.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/flags/iq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/flags/is.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/it.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/jm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/jo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/jp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/ke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/flags/km.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/kn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/kp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/kw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/la.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/lc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/lr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/ls.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/lt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/lu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/lv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/ma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/mc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/mf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/mg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/mh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/mk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/ml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/mm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/mn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/mo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/mq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/mr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/mu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/mv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/my.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/na.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/nc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ne.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ng.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/nl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/np.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/nr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/flags/pa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/ph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/pk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/pl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/pm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/pr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/ps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/pw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/qa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/flags/re.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/rw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/sb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/sc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/sd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/se.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/sg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/sj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/sk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/flags/sl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/sn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/so.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/sr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/st.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/flags/sy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/td.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/tf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/flags/tg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/th.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/tk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/tl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/tn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/to.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/flags/tr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/tt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/tw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/flags/tz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/flags/ua.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/uz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/flags/vc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/flags/ve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/flags/vn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/flags/wf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ws.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/ye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/yt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/flags/za.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /doc/build_from_source.md: -------------------------------------------------------------------------------- 1 | ## Build from source 2 | 3 | - Install [Git](https://git-scm.com/) 4 | - Install [Node.js](https://nodejs.org/en/) 5 | - Clone the repository 6 | - `git clone https://github.com/boi123212321/porn-vault.git` 7 | - Install dependencies 8 | - `npm install` 9 | - Build web app dependencies 10 | - `cd app` 11 | - `npm install` 12 | - Run web app in dev mode (in app/ folder) 13 | - `npm run serve` 14 | - Build web app (in app/ folder) 15 | - `npm run build` 16 | - Transpile server (in root folder) 17 | - `npm run transpile` 18 | - OR 19 | - `npm run watch` (same as above, but will retranspile every time the sources change) 20 | - Run server in dev mode (in root folder) 21 | - `npm run mon` 22 | - Run server in release mode (in root folder) 23 | - `npm run build` 24 | - And run the built executable in the release/ folder 25 | 26 | 27 | ## Debugging in Visual Studio Code 28 | 29 | 1. First transpile the sources with the `transpile` or `watch` script 30 | 31 | 2. Add the following to your `.vscode/launch.json` configuration in the project folder: 32 | ```json 33 | { 34 | "name": "Launch pre-transpiled build/ via nodemon", 35 | "type": "node", 36 | "request": "launch", 37 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/nodemon", 38 | "program": "${workspaceFolder}/build", 39 | "restart": true, 40 | "console": "externalTerminal", 41 | "internalConsoleOptions": "neverOpen" 42 | } 43 | ``` 44 | > This corresponds to the `mon` script 45 | 46 | 3. ???? 47 | 4. PROFIT!!! -------------------------------------------------------------------------------- /doc/img/actor_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/actor_collection.jpg -------------------------------------------------------------------------------- /doc/img/actor_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/actor_details.jpg -------------------------------------------------------------------------------- /doc/img/btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/btc.png -------------------------------------------------------------------------------- /doc/img/image_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/image_collection.jpg -------------------------------------------------------------------------------- /doc/img/image_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/image_details.jpg -------------------------------------------------------------------------------- /doc/img/movie_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/movie_collection.jpg -------------------------------------------------------------------------------- /doc/img/movie_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/movie_details.jpg -------------------------------------------------------------------------------- /doc/img/parent_studio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/parent_studio.jpg -------------------------------------------------------------------------------- /doc/img/plugin_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/plugin_filter.png -------------------------------------------------------------------------------- /doc/img/scene_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/scene_collection.jpg -------------------------------------------------------------------------------- /doc/img/scene_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/scene_details.jpg -------------------------------------------------------------------------------- /doc/img/studio_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/doc/img/studio_collection.jpg -------------------------------------------------------------------------------- /doc/import.md: -------------------------------------------------------------------------------- 1 | # Import format 2 | 3 | Add one or more folders to import from to `BULK_IMPORT_PATHS` in your config 4 | Then add .json or .yaml files containing data: 5 | 6 | ``` 7 | { 8 | "movies": { 9 | "[iid]": 10 | }, 11 | "scenes": { 12 | "[iid]": 13 | }, 14 | "actors": { 15 | "[iid]": 16 | }, 17 | "studios": { 18 | "[iid]": 19 | }, 20 | "labels": { 21 | "[iid]": 22 | }, 23 | "markers": { 24 | "[iid]": 25 | }, 26 | "customFields": { 27 | "[iid]": 28 | } 29 | } 30 | ``` 31 | 32 | IIDs can reference objects with IIDs **only** in that file, or, if a "real" ID is used, it will reference an object in the database. 33 | 34 | Check [here](https://github.com/boi123212321/porn-vault/tree/dev/src/import/schemas) for schemas. 35 | 36 | Use `--commit-import` to actually add stuff, omit, if you want to check the validity of your import. 37 | -------------------------------------------------------------------------------- /doc/pipe_plugins.md: -------------------------------------------------------------------------------- 1 | ## Pipe plugin results into another plugin 2 | 3 | Take this plugin as an example of an imaginary API that we have fetched data from: 4 | 5 | ```js 6 | // ./plugins/imaginaryapi.js 7 | module.exports = () => { 8 | return { 9 | labels: ["anal", "dp", "gangbang", "blonde"], 10 | }; 11 | }; 12 | ``` 13 | 14 | ```javascript 15 | // config.json 16 | 17 | { 18 | "plugins": { 19 | "events": { 20 | "actorCreated": ["imaginaryapi"] 21 | }, 22 | "register": { 23 | "imaginaryapi": { 24 | "path": "./plugins/imaginaryapi.js" 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | What if we don't want any hair color associated via a label? 32 | This can be solved by transforming the plugin result with another plugin, instead of filtering in the API plugin. 33 | 34 | ```js 35 | // ./plugins/nohaircolor.js 36 | 37 | module.exports = ({ data }) => { 38 | if (data.labels) { 39 | return { 40 | labels: data.labels.filter((s) => s != "blonde"), 41 | }; 42 | } 43 | return {}; 44 | }; 45 | ``` 46 | 47 | ```javascript 48 | // config.json 49 | 50 | { 51 | "plugins": { 52 | "events": { 53 | "actorCreated": ["imaginaryapi", "nohaircolor"] 54 | }, 55 | "register": { 56 | "imaginaryapi": { 57 | "path": "./plugins/imaginaryapi.js" 58 | }, 59 | "nohaircolor": { 60 | "path": "./plugins/nohaircolor.js" 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ### Result 68 | 69 | ![Result](https://github.com/boi123212321/porn-vault/blob/dev/doc/img/plugin_filter.png) 70 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["plugins/*", "assets/*"] 3 | } 4 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # We want a configuration file in /config so it can be stored in a persistant volume and not whiped out if we update the container. 3 | # Check if this symlink exists 4 | if [ ! -L config.json ] 5 | then 6 | if [ ! -f /config/config.json ] 7 | then 8 | echo "copying example configuration" 9 | mv config.json.example /config/config.json 10 | ln -s config/config.json config.json 11 | fi 12 | fi 13 | if [ ! -d /videos ] 14 | then 15 | mkdir /videos 16 | fi 17 | if [ ! -d /images ] 18 | then 19 | mkdir /images 20 | fi 21 | /porn-vault 22 | -------------------------------------------------------------------------------- /src/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server-express"; 2 | // import responseCachePlugin from "apollo-server-plugin-response-cache"; 3 | import express from "express"; 4 | 5 | // import { getConfig } from "./config"; 6 | import schema from "./graphql/types"; 7 | 8 | export function mountApolloServer(app: express.Application): void { 9 | // const config = getConfig(); 10 | const server = new ApolloServer({ 11 | plugins: [ 12 | /* responseCachePlugin() */ 13 | ], 14 | schema, 15 | /* cacheControl: { 16 | defaultMaxAge: Math.max(0, config.), 17 | }, */ 18 | context: ({ req }) => ({ 19 | req, 20 | }), 21 | }); 22 | server.applyMiddleware({ app, path: "/ql" }); 23 | } 24 | -------------------------------------------------------------------------------- /src/args.ts: -------------------------------------------------------------------------------- 1 | import yargs from "yargs"; 2 | 3 | import * as logger from "./logger"; 4 | 5 | const argv = yargs 6 | .boolean("process-queue") 7 | .boolean("commit-import") 8 | .boolean("ignore-integrity") 9 | .boolean("skip-compaction") 10 | .boolean("update-izzy") 11 | .boolean("update-gianna") 12 | .number("index-slice-size").argv; 13 | logger.log(argv); 14 | export default argv; 15 | -------------------------------------------------------------------------------- /src/config/validate.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | 3 | import { IConfig } from "../config/schema"; 4 | import * as logger from "../logger"; 5 | 6 | export function validateFFMPEGPaths(config: IConfig): void { 7 | if (config.binaries.ffmpeg) { 8 | const found = existsSync(config.binaries.ffmpeg); 9 | if (!found) { 10 | logger.error(`FFMPEG binary not found at ${config.binaries.ffmpeg}`); 11 | process.exit(1); 12 | } 13 | } else { 14 | logger.error(`No FFMPEG path defined in config.json`); 15 | process.exit(1); 16 | } 17 | 18 | if (config.binaries.ffprobe) { 19 | const found = existsSync(config.binaries.ffprobe); 20 | if (!found) { 21 | logger.error(`FFPROBE binary not found at ${config.binaries.ffprobe}`); 22 | process.exit(1); 23 | } 24 | } else { 25 | logger.error(`No FFPROBE path defined in config.json`); 26 | process.exit(1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/dvd_renderer.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { getConfig } from "./config/index"; 4 | import { renderHandlebars } from "./render"; 5 | import Image from "./types/image"; 6 | import Movie from "./types/movie"; 7 | import Studio from "./types/studio"; 8 | 9 | export async function dvdRenderer(req: express.Request, res: express.Response): Promise { 10 | const config = getConfig(); 11 | const movie = await Movie.getById(req.params.id); 12 | 13 | if (!movie) { 14 | res.status(404).send( 15 | await renderHandlebars("./views/error.html", { 16 | code: 404, 17 | message: `Movie ${req.params.id} not found`, 18 | }) 19 | ); 20 | } else { 21 | const color = movie.frontCover ? (await Image.getById(movie.frontCover))?.color : ""; 22 | 23 | const studioName = movie.studio ? (await Studio.getById(movie.studio))?.name : ""; 24 | 25 | const imageOrNull = function (id: string | null) { 26 | return id ? `/image/${id}?password=${config.auth.password}` : null; 27 | }; 28 | 29 | res.status(200).send( 30 | await renderHandlebars("./views/dvd-renderer.html", { 31 | color, 32 | movieName: movie.name, 33 | studioName, 34 | light: req.query.light === "true", 35 | frontCover: imageOrNull(movie.frontCover), 36 | backCover: imageOrNull(movie.backCover), 37 | spineCover: imageOrNull(movie.spineCover), 38 | }) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/exit.ts: -------------------------------------------------------------------------------- 1 | import { giannaProcess } from "./gianna"; 2 | import { izzyProcess } from "./izzy"; 3 | import * as logger from "./logger"; 4 | 5 | function killProcess(code = 0) { 6 | return () => { 7 | if (izzyProcess) { 8 | logger.log("Killing izzy..."); 9 | izzyProcess.kill(); 10 | } 11 | if (giannaProcess) { 12 | logger.log("Killing gianna..."); 13 | giannaProcess.kill(); 14 | } 15 | 16 | // When running tests, we want to be able to cleanup any services, 17 | // but we cannot overload the actual 'exit' otherwise mocha's 18 | // exit code will not reflect the actual result of the tests 19 | if (process.env.NODE_ENV !== "test") { 20 | process.exit(code); 21 | } 22 | }; 23 | } 24 | 25 | export function applyExitHooks(): void { 26 | process.on("exit", killProcess(0)); 27 | process.on("SIGTERM", killProcess(0)); 28 | process.on("SIGINT", killProcess(0)); 29 | process.on("SIGUSR1", killProcess(0)); 30 | process.on("SIGUSR2", killProcess(0)); 31 | process.on("uncaughtException", (e) => { 32 | console.log("Uncaught Exception..."); 33 | console.log(e.stack); 34 | killProcess(99)(); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/ffmpeg/screenshot.ts: -------------------------------------------------------------------------------- 1 | import ffmpeg from "fluent-ffmpeg"; 2 | 3 | export function singleScreenshot( 4 | video: string, 5 | output: string, 6 | time: number, 7 | maxWidth = 960 8 | ): Promise { 9 | return new Promise((resolve, reject) => { 10 | ffmpeg(video) 11 | .seekInput(time) 12 | .output(output) 13 | .outputOptions("-frames", "1") 14 | .size(`"${maxWidth}x?"`) 15 | .on("end", () => { 16 | resolve(output); 17 | }) 18 | .on("error", (err: Error) => { 19 | reject(err); 20 | }) 21 | .run(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/fs/index.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync } from "fs"; 2 | 3 | export const isDirectory = (path: string): boolean => lstatSync(path).isDirectory(); 4 | -------------------------------------------------------------------------------- /src/graphql/mutation.ts: -------------------------------------------------------------------------------- 1 | import ActorMutations from "./mutations/actor"; 2 | import CustomFieldMutations from "./mutations/custom_field"; 3 | import ImageMutations from "./mutations/image"; 4 | import LabelMutations from "./mutations/label"; 5 | import MarkerMutations from "./mutations/marker"; 6 | import MovieMutations from "./mutations/movie"; 7 | import SceneMutations from "./mutations/scene"; 8 | import StudioMutations from "./mutations/studio"; 9 | 10 | export default { 11 | ...ImageMutations, 12 | ...ActorMutations, 13 | ...LabelMutations, 14 | ...SceneMutations, 15 | ...MovieMutations, 16 | ...StudioMutations, 17 | ...MarkerMutations, 18 | ...CustomFieldMutations, 19 | }; 20 | -------------------------------------------------------------------------------- /src/graphql/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLJSONObject } from "graphql-type-json"; 2 | import GraphQLLong from "graphql-type-long"; 3 | 4 | import MutationResolver from "./mutation"; 5 | import ActorResolver from "./resolvers/actor"; 6 | import CustomFieldResolver from "./resolvers/custom_field"; 7 | import ImageResolver from "./resolvers/image"; 8 | import LabelResolver from "./resolvers/label"; 9 | import MarkerResolver from "./resolvers/marker"; 10 | import MovieResolver from "./resolvers/movie"; 11 | import QueryResolvers from "./resolvers/query"; 12 | import SceneResolver from "./resolvers/scene"; 13 | import SceneViewResolver from "./resolvers/scene_view"; 14 | import StudioResolver from "./resolvers/studio"; 15 | 16 | const resolvers = { 17 | Long: GraphQLLong, 18 | Object: GraphQLJSONObject, 19 | 20 | Actor: ActorResolver, 21 | Scene: SceneResolver, 22 | Image: ImageResolver, 23 | Query: QueryResolvers, 24 | Mutation: MutationResolver, 25 | Label: LabelResolver, 26 | Movie: MovieResolver, 27 | Studio: StudioResolver, 28 | CustomField: CustomFieldResolver, 29 | Marker: MarkerResolver, 30 | SceneView: SceneViewResolver, 31 | }; 32 | 33 | export default resolvers; 34 | -------------------------------------------------------------------------------- /src/graphql/resolvers/custom_field.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/graphql/resolvers/image.ts: -------------------------------------------------------------------------------- 1 | import Actor from "../../types/actor"; 2 | import Image from "../../types/image"; 3 | import Label from "../../types/label"; 4 | import Scene from "../../types/scene"; 5 | import Studio from "../../types/studio"; 6 | 7 | export default { 8 | async actors(image: Image): Promise { 9 | return await Image.getActors(image); 10 | }, 11 | async scene(image: Image): Promise { 12 | if (image.scene) return await Scene.getById(image.scene); 13 | return null; 14 | }, 15 | async labels(image: Image): Promise { 16 | return await Image.getLabels(image); 17 | }, 18 | async studio(image: Image): Promise { 19 | if (image.studio) return Studio.getById(image.studio); 20 | return null; 21 | }, 22 | color(image: Image): string | null { 23 | return Image.color(image) || null; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/graphql/resolvers/label.ts: -------------------------------------------------------------------------------- 1 | import Image from "../../types/image"; 2 | import Label from "../../types/label"; 3 | 4 | export default { 5 | async thumbnail(label: Label): Promise { 6 | if (label.thumbnail) return await Image.getById(label.thumbnail); 7 | return null; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/graphql/resolvers/marker.ts: -------------------------------------------------------------------------------- 1 | import Image from "../../types/image"; 2 | import Label from "../../types/label"; 3 | import Marker from "../../types/marker"; 4 | import Scene from "../../types/scene"; 5 | 6 | export default { 7 | async labels(marker: Marker): Promise { 8 | return await Marker.getLabels(marker); 9 | }, 10 | async thumbnail(marker: Marker): Promise { 11 | if (marker.thumbnail) return await Image.getById(marker.thumbnail); 12 | return null; 13 | }, 14 | async scene(marker: Marker): Promise { 15 | return Scene.getById(marker.scene); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/graphql/resolvers/scene_view.ts: -------------------------------------------------------------------------------- 1 | import Scene from "../../types/scene"; 2 | import SceneView from "../../types/watch"; 3 | 4 | export default { 5 | async scene(view: SceneView): Promise { 6 | return await Scene.getById(view.scene); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/image.ts: -------------------------------------------------------------------------------- 1 | import { imageCollection } from "../../../database"; 2 | import * as logger from "../../../logger"; 3 | import { searchImages } from "../../../search/image"; 4 | import Image from "../../../types/image"; 5 | 6 | export async function getImages( 7 | _: unknown, 8 | { query, seed }: { query: string | undefined; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: (Image | null)[]; 14 | } 15 | | undefined 16 | > { 17 | try { 18 | const timeNow = +new Date(); 19 | const result = await searchImages(query || "", seed); 20 | 21 | logger.log( 22 | `Search results: ${result.max_items} hits found in ${(Date.now() - timeNow) / 1000}s` 23 | ); 24 | 25 | const images = await imageCollection.getBulk(result.items); 26 | 27 | logger.log(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 28 | 29 | return { 30 | numItems: result.max_items, 31 | numPages: result.num_pages, 32 | items: images.filter(Boolean), 33 | }; 34 | } catch (error) { 35 | logger.error(error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/marker.ts: -------------------------------------------------------------------------------- 1 | import * as logger from "../../../logger"; 2 | import { searchMarkers } from "../../../search/marker"; 3 | import Marker from "../../../types/marker"; 4 | 5 | export async function getMarkers( 6 | _: unknown, 7 | { query, seed }: { query?: string; seed?: string } 8 | ): Promise< 9 | | { 10 | numItems: number; 11 | numPages: number; 12 | items: (Marker | null)[]; 13 | } 14 | | undefined 15 | > { 16 | try { 17 | const timeNow = +new Date(); 18 | const result = await searchMarkers(query || "", seed); 19 | 20 | logger.log( 21 | `Search results: ${result.max_items} hits found in ${(Date.now() - timeNow) / 1000}s` 22 | ); 23 | 24 | const markers = await Promise.all(result.items.map(Marker.getById)); 25 | logger.log(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 26 | 27 | return { 28 | numItems: result.max_items, 29 | numPages: result.num_pages, 30 | items: markers.filter(Boolean), 31 | }; 32 | } catch (error) { 33 | logger.error(error); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/movie.ts: -------------------------------------------------------------------------------- 1 | import { movieCollection } from "../../../database"; 2 | import * as logger from "../../../logger"; 3 | import { searchMovies } from "../../../search/movie"; 4 | import Movie from "../../../types/movie"; 5 | 6 | export async function getMovies( 7 | _: unknown, 8 | { query, seed }: { query?: string; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: (Movie | null)[]; 14 | } 15 | | undefined 16 | > { 17 | try { 18 | const timeNow = +new Date(); 19 | const result = await searchMovies(query || "", seed); 20 | 21 | logger.log( 22 | `Search results: ${result.max_items} hits found in ${(Date.now() - timeNow) / 1000}s` 23 | ); 24 | 25 | const movies = await movieCollection.getBulk(result.items); 26 | 27 | logger.log(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 28 | 29 | return { 30 | numItems: result.max_items, 31 | numPages: result.num_pages, 32 | items: movies.filter(Boolean), 33 | }; 34 | } catch (error) { 35 | logger.error(error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/scene.ts: -------------------------------------------------------------------------------- 1 | import { sceneCollection } from "../../../database"; 2 | import * as logger from "../../../logger"; 3 | import { searchScenes } from "../../../search/scene"; 4 | import Scene from "../../../types/scene"; 5 | 6 | export async function getScenes( 7 | _: unknown, 8 | { query, seed }: { query: string | undefined; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: (Scene | null)[]; 14 | } 15 | | undefined 16 | > { 17 | try { 18 | const timeNow = +new Date(); 19 | const result = await searchScenes(query || "", seed); 20 | 21 | logger.log( 22 | `Search results: ${result.max_items} hits found in ${(Date.now() - timeNow) / 1000}s` 23 | ); 24 | 25 | const scenes = await sceneCollection.getBulk(result.items); 26 | 27 | logger.log(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 28 | 29 | return { 30 | numItems: result.max_items, 31 | numPages: result.num_pages, 32 | items: scenes.filter(Boolean), 33 | }; 34 | } catch (error) { 35 | logger.error(error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/studio.ts: -------------------------------------------------------------------------------- 1 | import { studioCollection } from "../../../database"; 2 | import * as logger from "../../../logger"; 3 | import { searchStudios } from "../../../search/studio"; 4 | import Studio from "../../../types/studio"; 5 | 6 | export async function getStudios( 7 | _: unknown, 8 | { query, seed }: { query?: string; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: (Studio | null)[]; 14 | } 15 | | undefined 16 | > { 17 | try { 18 | const timeNow = +new Date(); 19 | const result = await searchStudios(query || "", seed); 20 | 21 | logger.log( 22 | `Search results: ${result.max_items} hits found in ${(Date.now() - timeNow) / 1000}s` 23 | ); 24 | 25 | const studios = await studioCollection.getBulk(result.items); 26 | 27 | logger.log(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 28 | 29 | return { 30 | numItems: result.max_items, 31 | numPages: result.num_pages, 32 | items: studios.filter(Boolean), 33 | }; 34 | } catch (error) { 35 | logger.error(error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graphql/schema/custom_field.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | extend type Query { 5 | getCustomFields: [CustomField!]! 6 | } 7 | 8 | enum CustomFieldType { 9 | NUMBER 10 | STRING 11 | BOOLEAN 12 | SINGLE_SELECT 13 | MULTI_SELECT 14 | } 15 | 16 | enum CustomFieldTarget { 17 | SCENES 18 | ACTORS 19 | MOVIES 20 | IMAGES 21 | STUDIOS 22 | ALBUMS 23 | } 24 | 25 | type CustomField { 26 | _id: String! 27 | name: String! 28 | target: [CustomFieldTarget!]! 29 | type: CustomFieldType! 30 | values: [String!] 31 | unit: String 32 | } 33 | 34 | extend type Mutation { 35 | createCustomField( 36 | name: String! 37 | target: [CustomFieldTarget!]! 38 | type: CustomFieldType! 39 | values: [String!] 40 | unit: String 41 | ): CustomField! 42 | 43 | updateCustomField(id: String!, name: String, values: [String!], unit: String): CustomField! 44 | 45 | removeCustomField(id: String!): Boolean! 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /src/graphql/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | scalar Long 5 | scalar Object 6 | scalar Upload 7 | 8 | type SceneView { 9 | _id: String! 10 | scene: Scene 11 | date: Long! 12 | } 13 | 14 | type Query { 15 | getQueueInfo: QueueInfo! 16 | getWatches(min: Long, max: Long): [SceneView!]! 17 | } 18 | 19 | type Mutation { 20 | hello: String! 21 | } 22 | 23 | input Crop { 24 | left: Int! 25 | top: Int! 26 | width: Int! 27 | height: Int! 28 | } 29 | 30 | type QueueInfo { 31 | length: Int! 32 | processing: Boolean! 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/graphql/schema/label.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | extend type Query { 5 | numLabels: Int! 6 | getLabels(type: String): [Label!]! 7 | getLabelById(id: String!): Label 8 | } 9 | 10 | type Label { 11 | _id: String! 12 | name: String! 13 | aliases: [String!]! 14 | addedOn: Long! 15 | 16 | # Resolvers 17 | thumbnail: Image 18 | } 19 | 20 | input LabelUpdateOpts { 21 | name: String 22 | aliases: [String!] 23 | thumbnail: String 24 | } 25 | 26 | extend type Mutation { 27 | addLabel(name: String!, aliases: [String!]): Label! 28 | updateLabels(ids: [String!]!, opts: LabelUpdateOpts!): [Label!]! 29 | removeLabels(ids: [String!]!): Boolean! 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/graphql/schema/marker.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | type Marker { 5 | _id: String! 6 | name: String! 7 | time: Int! 8 | rating: Int 9 | favorite: Boolean 10 | bookmark: Long 11 | 12 | # Resolvers 13 | labels: [Label!]! 14 | thumbnail: Image 15 | scene: Scene 16 | } 17 | 18 | type MarkerSearchResults { 19 | numItems: Int! 20 | numPages: Int! 21 | items: [Marker!]! 22 | } 23 | 24 | extend type Query { 25 | getMarkers(query: String, seed: String): MarkerSearchResults! 26 | } 27 | 28 | input MarkerUpdateOpts { 29 | favorite: Boolean 30 | bookmark: Long 31 | actors: [String!] 32 | name: String 33 | rating: Int 34 | labels: [String!] 35 | } 36 | 37 | extend type Mutation { 38 | createMarker( 39 | scene: String! 40 | name: String! 41 | time: Int! 42 | rating: Int 43 | favorite: Boolean 44 | bookmark: Long 45 | labels: [String!] 46 | ): Marker! 47 | updateMarkers(ids: [String!]!, opts: MarkerUpdateOpts!): [Marker!]! 48 | removeMarkers(ids: [String!]!): Boolean! 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /src/graphql/schema/movie.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | type MovieSearchResults { 5 | numItems: Int! 6 | numPages: Int! 7 | items: [Movie!]! 8 | } 9 | 10 | extend type Query { 11 | numMovies: Int! 12 | getMovies(query: String, seed: String): MovieSearchResults! 13 | getMovieById(id: String!): Movie 14 | } 15 | 16 | type Movie { 17 | _id: String! 18 | name: String! 19 | description: String 20 | addedOn: Long! 21 | releaseDate: Long 22 | favorite: Boolean! 23 | bookmark: Long 24 | customFields: Object! 25 | 26 | # Resolvers 27 | rating: Int # Inferred from scene ratings 28 | frontCover: Image 29 | backCover: Image 30 | spineCover: Image 31 | scenes: [Scene!]! 32 | actors: [Actor!]! 33 | labels: [Label!]! # Inferred from scene labels 34 | duration: Long 35 | size: Long 36 | studio: Studio 37 | } 38 | 39 | input MovieUpdateOpts { 40 | name: String 41 | description: String 42 | releaseDate: Long 43 | frontCover: String 44 | backCover: String 45 | spineCover: String 46 | favorite: Boolean 47 | bookmark: Long 48 | rating: Int 49 | scenes: [String!] 50 | studio: String 51 | customFields: Object 52 | } 53 | 54 | extend type Mutation { 55 | addMovie(name: String!, scenes: [String!]): Movie! 56 | updateMovies(ids: [String!]!, opts: MovieUpdateOpts!): [Movie!]! 57 | removeMovies(ids: [String!]!): Boolean! 58 | } 59 | `; 60 | -------------------------------------------------------------------------------- /src/graphql/schema/studio.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | type StudioSearchResults { 5 | numItems: Int! 6 | numPages: Int! 7 | items: [Studio!]! 8 | } 9 | 10 | extend type Query { 11 | numStudios: Int! 12 | getStudios(query: String, seed: String): StudioSearchResults! 13 | getStudioById(id: String!): Studio 14 | } 15 | 16 | type Studio { 17 | _id: String! 18 | name: String! 19 | description: String 20 | addedOn: Long! 21 | favorite: Boolean! 22 | bookmark: Long 23 | customFields: Object! 24 | aliases: [String!] 25 | 26 | # Resolvers 27 | parent: Studio 28 | substudios: [Studio!]! 29 | numScenes: Int! 30 | thumbnail: Image 31 | rating: Int # Inferred from scene ratings 32 | scenes: [Scene!]! 33 | labels: [Label!]! # Inferred from scene labels 34 | actors: [Actor!]! # Inferred from scene actors 35 | movies: [Movie!]! 36 | } 37 | 38 | input StudioUpdateOpts { 39 | name: String 40 | description: String 41 | thumbnail: String 42 | favorite: Boolean 43 | bookmark: Long 44 | parent: String 45 | labels: [String!] 46 | aliases: [String!] 47 | } 48 | 49 | extend type Mutation { 50 | addStudio(name: String!): Studio! 51 | updateStudios(ids: [String!]!, opts: StudioUpdateOpts!): [Studio!]! 52 | removeStudios(ids: [String!]!): Boolean! 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /src/graphql/types.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from "graphql-tools"; 2 | 3 | import RootResolver from "./resolvers"; 4 | import actorSchema from "./schema/actor"; 5 | import customFieldSchema from "./schema/custom_field"; 6 | import imageSchema from "./schema/image"; 7 | import indexSchema from "./schema/index"; 8 | import labelSchema from "./schema/label"; 9 | import markerSchema from "./schema/marker"; 10 | import movieSchema from "./schema/movie"; 11 | import sceneSchema from "./schema/scene"; 12 | import studioSchema from "./schema/studio"; 13 | 14 | export default makeExecutableSchema({ 15 | typeDefs: [ 16 | actorSchema, 17 | indexSchema, 18 | imageSchema, 19 | sceneSchema, 20 | studioSchema, 21 | movieSchema, 22 | labelSchema, 23 | customFieldSchema, 24 | markerSchema, 25 | ], 26 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 27 | // @ts-ignore 28 | resolvers: RootResolver, 29 | resolverValidationOptions: { 30 | requireResolversForResolveType: false, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/hash.ts: -------------------------------------------------------------------------------- 1 | export function randomString(length = 8): string { 2 | let result = ""; 3 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 4 | const charactersLength = characters.length; 5 | for (let i = 0; i < Math.max(1, length); i++) { 6 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 7 | } 8 | return result; 9 | } 10 | 11 | export function generateHash(): string { 12 | return new Date().valueOf().toString(36) + randomString(); 13 | } 14 | -------------------------------------------------------------------------------- /src/import/schemas/actor.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { isValidCountryCode } from "../../types/countries"; 4 | import { isValidDate, limitRating, stringArray, validCustomFields } from "./common"; 5 | 6 | export const actorSchema = new Schema({ 7 | name: { 8 | type: String, 9 | required: true, 10 | }, 11 | aliases: stringArray(false), 12 | description: { 13 | type: String, 14 | required: false, 15 | }, 16 | bornOn: { 17 | type: Number, 18 | required: false, 19 | use: { isValidDate }, 20 | }, 21 | thumbnail: { 22 | type: String, 23 | required: false, 24 | }, 25 | rating: { 26 | type: Number, 27 | required: false, 28 | use: { limitRating }, 29 | }, 30 | labels: stringArray(false), 31 | customFields: { 32 | required: false, 33 | type: Object, 34 | use: { validCustomFields }, 35 | }, 36 | bookmark: { 37 | required: false, 38 | type: Number, 39 | }, 40 | favorite: { 41 | required: false, 42 | type: Boolean, 43 | }, 44 | nationality: { 45 | required: false, 46 | type: String, 47 | use: { isValidCountryCode }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/import/schemas/common.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "../../types/utility"; 2 | 3 | export const isString = (i: unknown): i is string => typeof i === "string"; 4 | 5 | export function validCustomFields>(obj?: T | null): boolean { 6 | if (!obj) return true; 7 | return Object.values(obj).every(isString); 8 | } 9 | 10 | export function stringArray( 11 | required: boolean 12 | ): { 13 | required: boolean; 14 | type: ArrayConstructor; 15 | each: { 16 | type: StringConstructor; 17 | }; 18 | } { 19 | return { 20 | required, 21 | type: Array, 22 | each: { type: String }, 23 | }; 24 | } 25 | 26 | export function limitRating(i?: number | null): boolean { 27 | if (!i) return true; 28 | return i >= 0 && i <= 10; 29 | } 30 | 31 | export const isValidDate = (i?: number | null): boolean => { 32 | if (!i) return true; 33 | if (i < 0) return false; 34 | const d = new Date(i); 35 | return d instanceof Date && !isNaN((d)); 36 | }; 37 | -------------------------------------------------------------------------------- /src/import/schemas/custom_field.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { CustomFieldType } from "../../types/custom_field"; 4 | import { stringArray } from "./common"; 5 | 6 | export const customFieldSchema = new Schema({ 7 | name: { 8 | type: String, 9 | required: true, 10 | }, 11 | type: { 12 | type: String, 13 | required: true, 14 | enum: Object.keys(CustomFieldType), 15 | }, 16 | values: { 17 | ...stringArray(false), 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/import/schemas/label.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { stringArray } from "./common"; 4 | 5 | export const labelSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | aliases: stringArray(false), 11 | }); 12 | -------------------------------------------------------------------------------- /src/import/schemas/marker.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { limitRating, stringArray } from "./common"; 4 | 5 | export const markerSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | time: { 11 | type: Number, 12 | required: true, 13 | }, 14 | scene: { 15 | type: String, 16 | required: true, 17 | }, 18 | rating: { 19 | type: Number, 20 | required: false, 21 | use: { limitRating }, 22 | }, 23 | bookmark: { 24 | required: false, 25 | type: Number, 26 | }, 27 | favorite: { 28 | required: false, 29 | type: Boolean, 30 | }, 31 | labels: stringArray(false), 32 | }); 33 | -------------------------------------------------------------------------------- /src/import/schemas/movie.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { isValidDate, limitRating, stringArray, validCustomFields } from "./common"; 4 | 5 | export const movieSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | description: { 11 | type: String, 12 | required: false, 13 | }, 14 | frontCover: { 15 | type: String, 16 | required: false, 17 | }, 18 | backCover: { 19 | type: String, 20 | required: false, 21 | }, 22 | spineCover: { 23 | type: String, 24 | required: false, 25 | }, 26 | studio: { 27 | type: String, 28 | required: false, 29 | }, 30 | releaseDate: { 31 | type: Number, 32 | required: false, 33 | use: { isValidDate }, 34 | }, 35 | rating: { 36 | type: Number, 37 | required: false, 38 | use: { limitRating }, 39 | }, 40 | scenes: stringArray(false), 41 | customFields: { 42 | required: false, 43 | type: Object, 44 | use: { validCustomFields }, 45 | }, 46 | bookmark: { 47 | required: false, 48 | type: Number, 49 | }, 50 | favorite: { 51 | required: false, 52 | type: Boolean, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/import/schemas/scene.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { isValidDate, limitRating, stringArray, validCustomFields } from "./common"; 4 | 5 | export const sceneSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | path: { 11 | type: String, 12 | required: true, 13 | }, 14 | description: { 15 | type: String, 16 | required: false, 17 | }, 18 | thumbnail: { 19 | type: String, 20 | required: false, 21 | }, 22 | studio: { 23 | type: String, 24 | required: false, 25 | }, 26 | releaseDate: { 27 | type: Number, 28 | required: false, 29 | use: { isValidDate }, 30 | }, 31 | rating: { 32 | type: Number, 33 | required: false, 34 | use: { limitRating }, 35 | }, 36 | actors: stringArray(false), 37 | labels: stringArray(false), 38 | customFields: { 39 | required: false, 40 | type: Object, 41 | use: { validCustomFields }, 42 | }, 43 | bookmark: { 44 | required: false, 45 | type: Number, 46 | }, 47 | favorite: { 48 | required: false, 49 | type: Boolean, 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /src/import/schemas/studio.ts: -------------------------------------------------------------------------------- 1 | import Schema from "validate"; 2 | 3 | import { limitRating, stringArray } from "./common"; 4 | 5 | export const studioSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | parent: { 11 | type: String, 12 | required: false, 13 | }, 14 | aliases: stringArray(false), 15 | thumbnail: { 16 | type: String, 17 | required: false, 18 | }, 19 | rating: { 20 | type: Number, 21 | required: false, 22 | use: { limitRating }, 23 | }, 24 | labels: stringArray(false), 25 | bookmark: { 26 | required: false, 27 | type: Number, 28 | }, 29 | favorite: { 30 | required: false, 31 | type: Boolean, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/mem.ts: -------------------------------------------------------------------------------- 1 | import v8 from "v8"; 2 | 3 | import * as logger from "./logger"; 4 | 5 | export function printMaxMemory(): void { 6 | logger.message( 7 | `Max. memory: ${Math.round(v8.getHeapStatistics().total_available_size / 1024 / 1024)} MB` 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/middlewares/cors.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | export default function cors( 4 | req: express.Request, 5 | res: express.Response, 6 | next: express.NextFunction 7 | ): void { 8 | res.header("Access-Control-Allow-Origin", "*"); 9 | res.header("Access-Control-Allow-Headers", "*"); 10 | res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); 11 | 12 | // intercept OPTIONS method 13 | if (req.method === "OPTIONS") { 14 | res.sendStatus(200); 15 | } else { 16 | next(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/render.ts: -------------------------------------------------------------------------------- 1 | import Handlebars from "handlebars"; 2 | 3 | import { readFileAsync } from "./fs/async"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export async function renderHandlebars( 7 | file: string, 8 | context: TContext 9 | ): Promise { 10 | const text = await readFileAsync(file, "utf-8"); 11 | return Handlebars.compile(text)(context); 12 | } 13 | -------------------------------------------------------------------------------- /src/search/index.ts: -------------------------------------------------------------------------------- 1 | import { buildActorIndex } from "./actor"; 2 | import { buildImageIndex } from "./image"; 3 | import { buildMarkerIndex } from "./marker"; 4 | import { buildMovieIndex } from "./movie"; 5 | import { buildSceneIndex } from "./scene"; 6 | import { buildStudioIndex } from "./studio"; 7 | 8 | export async function buildIndices(): Promise { 9 | await buildSceneIndex(); 10 | await buildActorIndex(); 11 | await buildMovieIndex(); 12 | await buildStudioIndex(); 13 | await buildImageIndex(); 14 | await buildMarkerIndex(); 15 | } 16 | -------------------------------------------------------------------------------- /src/types/countries.ts: -------------------------------------------------------------------------------- 1 | import countries, { ICountry } from "../data/countries"; 2 | 3 | const countryMap = (() => { 4 | const map = {} as Record; 5 | countries.forEach((c) => { 6 | map[c.alpha2] = c; 7 | }); 8 | return map; 9 | })(); 10 | 11 | export function getNationality(str: string): ICountry { 12 | return countryMap[str.toUpperCase()]; 13 | } 14 | 15 | export function isValidCountryCode(str: string): boolean { 16 | return !!getNationality(str); 17 | } 18 | -------------------------------------------------------------------------------- /src/types/movie_scene.ts: -------------------------------------------------------------------------------- 1 | import { movieSceneCollection } from "../database/index"; 2 | import { generateHash } from "../hash"; 3 | 4 | export default class MovieScene { 5 | _id: string; 6 | movie: string; 7 | scene: string; 8 | 9 | constructor(movie: string, scene: string) { 10 | this._id = "ms_" + generateHash(); 11 | this.movie = movie; 12 | this.scene = scene; 13 | } 14 | 15 | static async getAll(): Promise { 16 | return movieSceneCollection.getAll(); 17 | } 18 | 19 | static async getByMovie(movie: string): Promise { 20 | return movieSceneCollection.query("movie-index", movie); 21 | } 22 | 23 | static async getByScene(scene: string): Promise { 24 | return movieSceneCollection.query("scene-index", scene); 25 | } 26 | 27 | static async get(from: string, to: string): Promise { 28 | const fromReferences = await movieSceneCollection.query("movie-index", from); 29 | return fromReferences.find((r) => r.scene === to); 30 | } 31 | 32 | static async removeByScene(id: string): Promise { 33 | for (const ref of await MovieScene.getByScene(id)) { 34 | await MovieScene.removeById(ref._id); 35 | } 36 | } 37 | 38 | static async removeByMovie(id: string): Promise { 39 | for (const ref of await MovieScene.getByMovie(id)) { 40 | await MovieScene.removeById(ref._id); 41 | } 42 | } 43 | 44 | static async removeById(_id: string): Promise { 45 | await movieSceneCollection.remove(_id); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/types/watch.ts: -------------------------------------------------------------------------------- 1 | import { viewCollection } from "../database/index"; 2 | import { generateHash } from "../hash"; 3 | 4 | export default class SceneView { 5 | _id: string; 6 | date: number; 7 | scene: string; 8 | 9 | static async getByScene(sceneId: string): Promise { 10 | const items = await viewCollection.query("scene-index", sceneId); 11 | return items.sort((a, b) => a.date - b.date); 12 | } 13 | 14 | static async getCount(sceneId: string): Promise { 15 | return (await SceneView.getByScene(sceneId)).length; 16 | } 17 | 18 | static async getAll(): Promise { 19 | const items = await viewCollection.getAll(); 20 | return items.sort((a, b) => a.date - b.date); 21 | } 22 | 23 | constructor(sceneId: string, date: number) { 24 | this._id = "sc_" + generateHash(); 25 | this.date = date; 26 | this.scene = sceneId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/typings/js-sha512.d.ts: -------------------------------------------------------------------------------- 1 | declare module "js-sha512" { 2 | export function sha512(str: string): string; 3 | } 4 | -------------------------------------------------------------------------------- /src/typings/merge-img.d.ts: -------------------------------------------------------------------------------- 1 | declare module "merge-img" { 2 | export default function mergeImg(files: string[]): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /test/config/index.fixture.ts: -------------------------------------------------------------------------------- 1 | import YAML from "yaml"; 2 | 3 | export const preserve = { 4 | json: { 5 | parse: (str: string) => JSON.parse(str), 6 | stringify: (str: any) => JSON.stringify(str, null, 2), 7 | }, 8 | yaml: { 9 | parse: (str: string) => YAML.parse(str), 10 | stringify: (str: any) => YAML.stringify(str), 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /test/config/schema.fixture.ts: -------------------------------------------------------------------------------- 1 | import defaultConfig from "../../src/config/default"; 2 | 3 | export const invalidConfig = { 4 | ...defaultConfig, 5 | auth: false, 6 | }; 7 | -------------------------------------------------------------------------------- /test/config/schema.spec.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | 3 | import { assert } from "chai"; 4 | 5 | import defaultConfig from "../../src/config/default"; 6 | import { isValidConfig } from "../../src/config/schema"; 7 | import { invalidConfig } from "./schema.fixture"; 8 | 9 | describe("schema", () => { 10 | describe("isValidConfig", () => { 11 | it("default config is valid", () => { 12 | const validationResult = isValidConfig(defaultConfig); 13 | assert.notInstanceOf(validationResult, Error); 14 | assert.isTrue(validationResult); 15 | }); 16 | 17 | 18 | // Since we are simply using zod's validation, without custom value validation, 19 | // we only need once test to verify the 'isValidConfig' function, 20 | // since we do not want to duplicate the tests of the 'zod' package 21 | it("dummy invalid config fails validation", () => { 22 | const validationResult = isValidConfig(invalidConfig); 23 | assert.instanceOf(validationResult, Error); 24 | assert.isNotTrue(validationResult); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixtures/files/image001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/image001.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/image002.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/image003.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/image004.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image005.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/image005.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image006.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/some_folder/image006.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image007.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/some_folder/image007.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image008.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/some_folder/image008.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image009.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/some_folder/image009.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image010.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyyberFroggy/porn-vault-1/bbe3dfb2ee7948c6bb5b71dc57ad9423ec005da0/test/fixtures/files/some_folder/image010.jpg -------------------------------------------------------------------------------- /test/fixtures/files/video001.mp4: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/matching_label.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | [ 3 | "jill kassidy swallowed", 4 | { 5 | name: "Anal", 6 | aliases: [], 7 | }, 8 | false, 9 | ], 10 | [ 11 | "Layla Love All Anal Blonde", 12 | { 13 | name: "Anal", 14 | aliases: [], 15 | }, 16 | true, 17 | ], 18 | [ 19 | "Chanel Grey's First DP", 20 | { 21 | name: "Anal", 22 | aliases: ["dp"], 23 | }, 24 | true, 25 | ], 26 | [ 27 | "7on1 Double Anal GangBang with Kira Thorn", 28 | { 29 | name: "Double penetration", 30 | aliases: ["regex:double.*"], 31 | }, 32 | true, 33 | ], 34 | [ 35 | "7on1 Double Anal GangBang with Kira Thorn", 36 | { 37 | name: "Double penetration", 38 | aliases: ["double.*"], 39 | }, 40 | true, 41 | ], 42 | [ 43 | "7on1 Double Anal GangBang with Kira Thorn", 44 | { 45 | name: "Double anal", 46 | aliases: [], 47 | }, 48 | true, 49 | ], 50 | [ 51 | "7on1 Double Anal GangBang with Kira Thorn", 52 | { 53 | name: "dap", 54 | aliases: ["double anal"], 55 | }, 56 | true, 57 | ], 58 | [ 59 | "7on1 Double Anal GangBang with Kira Thorn", 60 | { 61 | name: "dap", 62 | aliases: ["regex:double anal"], 63 | }, 64 | false, 65 | ], 66 | ] as [string, { name: string; aliases: string[] }, boolean][]; 67 | -------------------------------------------------------------------------------- /test/fixtures/remove_extension.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | ["videofile.mp4", "videofile"], 3 | ["videofile", "videofile"], 4 | ["./somewhere/else/video.mp4", "./somewhere/else/video"], 5 | ["./somewhere/else/video", "./somewhere/else/video"], 6 | ]; 7 | -------------------------------------------------------------------------------- /test/fixtures/strip_string.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | ["original string", "originalstring"], 3 | [ 4 | "Jill.And.Gina.In.A.Sloppy.Gargling.Suck.Party.Gina.Valentina.&.Jill.Kassidy.Swallowed.mp4", 5 | "jillandginainasloppygarglingsuckpartyginavalentinajillkassidyswallowedmp4", 6 | ], 7 | ["/data/paige owens.mp4", "/data/paigeowensmp4"], 8 | [ 9 | "Jill And Gina In A Sloppy Gargling Suck Party - gina Valentina&jill Kassidy - Swallowed [blowjob, threesome]", 10 | "jillandginainasloppygarglingsuckparty-ginavalentinajillkassidy-swallowed[blowjob,threesome]", 11 | ], 12 | [ 13 | "Jill And Gina In A Sloppy Gargling Suck Party - gina Valentina&jill Kassidy - Swallowed (blowjob, threesome)", 14 | "jillandginainasloppygarglingsuckparty-ginavalentinajillkassidy-swallowed(blowjob,threesome)", 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /test/fixtures/url_ext.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | ["https://freeones.xxx/avatar.jpg", ".jpg"], 3 | ["https://freeones.xxx/avatar.jpg?test=214", ".jpg"], 4 | ["https://freeones.xxx/avatar.png?test=214#weird.hash", ".png"], 5 | ["https://freeones.xxx/avatar.png#weird.hash", ".png"], 6 | ] as [string, string][]; 7 | -------------------------------------------------------------------------------- /test/fixtures/walk.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: "test/fixtures/files", 4 | exclude: [], 5 | extensions: [".jpg"], 6 | expected: { 7 | num: 10, 8 | }, 9 | }, 10 | { 11 | path: "test/fixtures/files", 12 | exclude: [], 13 | extensions: [".mp4"], 14 | expected: { 15 | num: 1, 16 | }, 17 | }, 18 | { 19 | path: "test/fixtures/files", 20 | exclude: [], 21 | extensions: [".jpg", ".mp4"], 22 | expected: { 23 | num: 11, 24 | }, 25 | }, 26 | { 27 | path: "test/fixtures/files", 28 | exclude: ["some_"], 29 | extensions: [".jpg"], 30 | expected: { 31 | num: 5, 32 | }, 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /test/init.ts: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | 4 | chai.use(chaiAsPromised); 5 | -------------------------------------------------------------------------------- /test/url_ext.spec.ts: -------------------------------------------------------------------------------- 1 | import { extensionFromUrl } from "../src/types/utility"; 2 | import tests from "./fixtures/url_ext.fixture"; 3 | import { expect } from "chai"; 4 | 5 | describe("Parse URL file extension", () => { 6 | for (const test of tests) { 7 | it(`Should get ${test[1]} from ${test[0]}`, async () => { 8 | expect(extensionFromUrl(test[0])).to.equal(test[1]); 9 | }); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdir } from "fs"; 2 | import { resolve } from "path"; 3 | import { promisify } from "util"; 4 | 5 | import { rimrafAsync } from "../src/fs/async"; 6 | 7 | // Assume these work perfectly 8 | export const mkdirAsync = promisify(mkdir); 9 | 10 | export const TEST_TEMP_DIR = resolve(process.cwd(), "temp"); 11 | 12 | export async function createTempTestingDir() { 13 | if (!existsSync(TEST_TEMP_DIR)) { 14 | await mkdirAsync(TEST_TEMP_DIR); 15 | } 16 | } 17 | 18 | export async function unlinkTempTestingDir() { 19 | if (existsSync(TEST_TEMP_DIR)) { 20 | await rimrafAsync(TEST_TEMP_DIR); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 27 | 28 | 29 |
30 |

{{ code }}

31 |

{{{ message }}}

32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /views/setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Setting up... 7 | 24 | 25 | 26 |
27 |

{{ message }}

28 |
29 | 30 | 41 | 42 | 43 | --------------------------------------------------------------------------------