├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build-app.yml │ ├── build-page.yml │ ├── build-server.yml │ ├── docker.yml │ ├── release.yml │ └── version.yml ├── .gitignore ├── .gitlab-ci.yml ├── .mocharc.js ├── .nvmrc ├── .nycrc ├── .prettierrc.js ├── LICENSE ├── README.md ├── app ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── config │ └── dev.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ └── system.ts │ ├── apollo.ts │ ├── components │ │ ├── ActorGrid.vue │ │ ├── ActorSelector.vue │ │ ├── AppBar │ │ │ ├── ActorDetails.vue │ │ │ ├── MovieDetails.vue │ │ │ ├── SceneDetails.vue │ │ │ └── StudioDetails.vue │ │ ├── BindFavicon.vue │ │ ├── BindTitle.vue │ │ ├── Cards │ │ │ ├── Actor.vue │ │ │ ├── Image.vue │ │ │ ├── Marker.vue │ │ │ ├── Movie.vue │ │ │ ├── Scene.vue │ │ │ └── Studio.vue │ │ ├── Code.vue │ │ ├── Collabs.vue │ │ ├── CreatedCustomField.vue │ │ ├── CustomFieldCreator.vue │ │ ├── CustomFieldFilter.vue │ │ ├── CustomFieldSelector.vue │ │ ├── DVDRenderer.vue │ │ ├── DateInput.vue │ │ ├── Divider.vue │ │ ├── Flag.vue │ │ ├── Footer.vue │ │ ├── HomeWidgets │ │ │ ├── ActorLabelUsage.vue │ │ │ ├── Base.vue │ │ │ ├── QueueInfo.vue │ │ │ ├── RemainingTime.vue │ │ │ ├── Scan.vue │ │ │ ├── SceneLabelUsage.vue │ │ │ ├── SearchTimes.vue │ │ │ ├── Stats.vue │ │ │ ├── TopActors.vue │ │ │ └── UnwatchedActors.vue │ │ ├── ImageUploader.vue │ │ ├── LabelFilter.vue │ │ ├── LabelGroup.vue │ │ ├── LabelSelector.vue │ │ ├── Lightbox.vue │ │ ├── Loading.vue │ │ ├── MarkerItem.vue │ │ ├── NoResults.vue │ │ ├── Plugins │ │ │ └── Item.vue │ │ ├── Rating.vue │ │ ├── SceneSelector.vue │ │ ├── SceneUploader.vue │ │ ├── SettingsWrapper.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 │ │ └── searchState.ts │ └── views │ │ ├── ActorDetails.vue │ │ ├── Actors.vue │ │ ├── Home.vue │ │ ├── Images.vue │ │ ├── Labels.vue │ │ ├── Logs.vue │ │ ├── Markers.vue │ │ ├── MovieDVD.vue │ │ ├── MovieDetails.vue │ │ ├── Movies.vue │ │ ├── Plugins.vue │ │ ├── SceneDetails.vue │ │ ├── Scenes.vue │ │ ├── Settings │ │ ├── Metadata.vue │ │ ├── Settings.vue │ │ ├── Status.vue │ │ ├── System.vue │ │ └── UI.vue │ │ ├── Setup.vue │ │ ├── StudioDetails.vue │ │ ├── Studios.vue │ │ └── Views.vue ├── tsconfig.json └── vue.config.js ├── assets ├── broken.png ├── bump.jpg ├── favicon.png ├── favicon_mono.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 └── version.json ├── codecov.yml ├── config.example.json ├── doc └── img │ ├── actor_collection.jpg │ ├── actor_details.jpg │ ├── btc.png │ ├── image_collection.jpg │ ├── image_details.jpg │ ├── movie_collection.jpg │ ├── movie_details.jpg │ ├── parent_studio.jpg │ ├── scene_collection.jpg │ ├── scene_details.jpg │ └── studio_collection.jpg ├── docker ├── Dockerfile.debian └── root │ └── etc │ ├── cont-init.d │ ├── 40-init-dirs │ └── 50-gid-video │ └── services.d │ └── porn-vault │ ├── finish │ └── run ├── gulpfile.ts ├── imagemagick.md ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── app.ts ├── args.ts ├── backup.ts ├── binaries │ ├── ffmpeg-download.ts │ ├── imagemagick.ts │ └── izzy.ts ├── config │ ├── default.ts │ ├── index.ts │ ├── schema.ts │ └── validate.ts ├── data │ └── countries.ts ├── database │ ├── index.ts │ └── internal │ │ └── index.ts ├── env.ts ├── exit.ts ├── extractor.ts ├── ffmpeg │ ├── ffprobe.ts │ └── screenshot.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 ├── index.ts ├── matching │ ├── matcher.ts │ ├── stringMatcher.ts │ └── wordMatcher.ts ├── middlewares │ ├── apollo.ts │ ├── cors.ts │ └── password.ts ├── plugins │ ├── context.ts │ ├── events │ │ ├── actor.ts │ │ ├── movie.ts │ │ ├── scene.ts │ │ └── studio.ts │ ├── index.ts │ ├── register.ts │ ├── store.ts │ ├── types.ts │ └── validate.ts ├── queue │ ├── check.ts │ └── processing.ts ├── queue_loop.ts ├── queue_router.ts ├── routers │ ├── config.ts │ ├── media.ts │ ├── scan.ts │ ├── scene.ts │ ├── static.ts │ └── system.ts ├── scanner.ts ├── search │ ├── actor.ts │ ├── common.ts │ ├── image.ts │ ├── index.ts │ ├── internal │ │ ├── buildIndex.ts │ │ └── constants.ts │ ├── marker.ts │ ├── movie.ts │ ├── scene.ts │ └── studio.ts ├── server.ts ├── setup.ts ├── startup.ts ├── static.ts ├── transcode │ ├── copyMp4.ts │ ├── mp4.ts │ ├── transcoder.ts │ └── webm.ts ├── types │ ├── actor.ts │ ├── actor_reference.ts │ ├── common.ts │ ├── countries.ts │ ├── custom_field.ts │ ├── image.ts │ ├── label.ts │ ├── labelled_item.ts │ ├── marker.ts │ ├── movie.ts │ ├── movie_scene.ts │ ├── scene.ts │ ├── studio.ts │ └── watch.ts ├── typings │ └── js-sha512.d.ts ├── utils │ ├── async.ts │ ├── download.ts │ ├── fs │ │ ├── async.ts │ │ └── index.ts │ ├── hash.ts │ ├── http.ts │ ├── logger.ts │ ├── mem.ts │ ├── misc.ts │ ├── path.ts │ ├── render.ts │ ├── string.ts │ └── types.ts └── version.ts ├── test ├── config │ ├── index.fixture.ts │ ├── index.spec.ts │ ├── schema.fixture.ts │ └── schema.spec.ts ├── fixtures │ ├── files │ │ ├── .hidden │ │ │ └── image100.jpg │ │ ├── dynamic │ │ │ └── .gitkeep │ │ ├── dynamicTestFiles.ts │ │ ├── image001.jpg │ │ ├── image002.jpg │ │ ├── image003.jpg │ │ ├── image004.jpg │ │ ├── image005.jpg │ │ ├── some_folder │ │ │ ├── image006.jpg │ │ │ ├── image007.jpg │ │ │ ├── image008.jpg │ │ │ ├── image009.jpg │ │ │ └── image010.jpg │ │ └── video001.mp4 │ └── walk.fixture.ts ├── graphql │ ├── mutations │ │ ├── actor.spec.ts │ │ ├── image.spec.ts │ │ ├── label.spec.ts │ │ ├── movie.spec.ts │ │ ├── scene.spec.ts │ │ └── studio.spec.ts │ └── resolvers │ │ └── scene.spec.ts ├── init.ts ├── matching │ ├── fixtures │ │ ├── matching_actor.fixture.ts │ │ ├── matching_label.fixture.ts │ │ ├── string_filter.fixture.ts │ │ ├── strip_string.fixture.ts │ │ └── wordMatcher.fixtures.ts │ ├── matcher.spec.ts │ ├── stringMatcher.spec.ts │ ├── studio.spec.ts │ └── wordMatcher.spec.ts ├── plugins │ ├── events │ │ ├── actor.spec.ts │ │ ├── movie.spec.ts │ │ ├── scene.spec.ts │ │ └── studio.spec.ts │ ├── fixtures │ │ ├── actor_plugin.fixture.js │ │ ├── actor_plugin.fixture.ts │ │ ├── movie_plugin.fixture.js │ │ ├── movie_plugin.fixture.ts │ │ ├── scene_plugin.fixture.js │ │ ├── scene_plugin.fixture.ts │ │ ├── studio │ │ │ └── recursive.fixture.js │ │ ├── studio_plugin.fixture.js │ │ └── studio_plugin.fixture.ts │ └── initPluginFixtures.ts ├── root.spec.ts ├── search │ ├── actor.spec.ts │ ├── movie.spec.ts │ ├── scene.spec.ts │ └── studio.spec.ts ├── testServer.ts ├── transcode │ ├── copyMp4.spec.ts │ ├── mp4.spec.ts │ └── webm.spec.ts ├── types │ ├── actor.spec.ts │ ├── marker.spec.ts │ ├── scene.spec.ts │ └── studio.spec.ts ├── util.ts ├── util │ ├── async.spec.ts │ ├── download.spec.ts │ ├── fixtures │ │ ├── mergeMissingProperties.fixtures.ts │ │ └── removeUnknownProperties.fixtures.ts │ ├── hash.spec.ts │ ├── mergeMissingProperties.spec.ts │ ├── rating.spec.ts │ ├── removeUnknownProperties.spec.ts │ └── types.spec.ts ├── utils │ ├── fixtures │ │ ├── array_diff.fixtures.ts │ │ ├── filter_invalid_aliases.fixtures.ts │ │ ├── generate_timestamps.fixtures.ts │ │ ├── get_extension.fixture.ts │ │ ├── is_array_eq.fixtures.ts │ │ ├── remove_extension.fixture.ts │ │ └── url_ext.fixture.ts │ ├── misc.spec.ts │ ├── string.spec.ts │ └── url_ext.spec.ts └── walk.spec.ts ├── tsconfig.dev.json ├── tsconfig.json ├── version.js └── views ├── error.html └── signin.html /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | backups 4 | library 5 | ffprobe 6 | ffmpeg 7 | izzy -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dvd_renderer.ts 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: pornvault 2 | custom: 3 | - https://en.cryptobadges.io/donate/1Bw82zC5FnVtw93ZrcALQTeZBXgtVWH75n 4 | - https://en.cryptobadges.io/donate/0x1138fb93fC9e3bAc3ab36949C2c806562bFDb621 5 | -------------------------------------------------------------------------------- /.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 | - "**" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js 14.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 14.x 20 | - name: Test app 21 | run: | 22 | npm ci 23 | npm run install:app 24 | npm run build:app 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/workflows/build-page.yml: -------------------------------------------------------------------------------- 1 | name: Build page & version 2 | 3 | on: 4 | push: 5 | branches: 6 | - docs 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 14.x 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 14.x 17 | - name: Build page 18 | run: | 19 | cd page 20 | npm install 21 | npm run docs:build 22 | - uses: stefanzweifel/git-auto-commit-action@v4 23 | with: 24 | commit_message: Build page 25 | -------------------------------------------------------------------------------- /.github/workflows/build-server.yml: -------------------------------------------------------------------------------- 1 | name: Build server 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | build: 13 | timeout-minutes: 15 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js 14.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14.x 21 | - name: Configure sysctl limits 22 | run: | 23 | sudo swapoff -a 24 | sudo sysctl -w vm.swappiness=1 25 | sudo sysctl -w fs.file-max=262144 26 | sudo sysctl -w vm.max_map_count=262144 27 | 28 | - name: Runs Elasticsearch 29 | uses: elastic/elastic-github-actions/elasticsearch@master 30 | with: 31 | stack-version: 7.9.0 32 | - name: Test server 33 | run: | 34 | npm install 35 | npm run lint 36 | npm run transpile:prod 37 | npm run coverage:silent 38 | env: 39 | CI: true 40 | - uses: codecov/codecov-action@v1 41 | with: 42 | file: "./coverage/coverage-final.json" 43 | name: codecov 44 | fail_ci_if_error: true 45 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Version 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 14.x 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 14.x 14 | - run: node version 15 | - uses: stefanzweifel/git-auto-commit-action@v4 16 | with: 17 | commit_message: Write version -------------------------------------------------------------------------------- /.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 | 27 | # Files 28 | config\.json 29 | config.testenv.json 30 | /ffmpeg 31 | /ffprobe 32 | ffmpeg.exe 33 | ffprobe.exe 34 | izzy 35 | izzy.exe 36 | config.test.json 37 | config.test.yaml 38 | config\.old\.yaml 39 | config\.yaml 40 | images\.json 41 | 42 | # Folders 43 | /test/library 44 | /library 45 | bin/ 46 | app/dist/ 47 | release/ 48 | releases/ 49 | tmp/ 50 | backups/ 51 | /plugins/ 52 | imports/ 53 | .vscode 54 | build/ 55 | 56 | config.merged.json 57 | config.test.merged.json 58 | config.merged.yaml 59 | config.test.merged.yaml 60 | 61 | config\.old\.yaml 62 | 63 | .nyc_output 64 | 65 | test/fixtures/files/dynamic/* 66 | *audit.json 67 | dist 68 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - docker_latest 4 | 5 | default: 6 | image: node:16 7 | 8 | variables: 9 | CI: "true" 10 | DOCKER_TLS_CERTDIR: "" 11 | 12 | app: 13 | stage: build 14 | only: 15 | refs: 16 | - dev 17 | - "merge_requests" 18 | script: 19 | - npm ci 20 | - npm run install:app 21 | - npm run build:app 22 | 23 | server: 24 | stage: build 25 | only: 26 | refs: 27 | - dev 28 | - "merge_requests" 29 | services: 30 | - name: "docker.elastic.co/elasticsearch/elasticsearch:7.17.0" 31 | alias: "elasticsearch" 32 | command: 33 | ["bin/elasticsearch", "-Expack.security.enabled=false", "-Ediscovery.type=single-node"] 34 | variables: 35 | ES_JAVA_OPTS: "-Xms200m -Xmx400m" 36 | script: 37 | - npm install 38 | - npm run transpile:prod 39 | - apt-get install -y imagemagick 40 | - npm run coverage:silent 41 | #after_script: 42 | # - bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN -f ./coverage/coverage-final.json 43 | 44 | release_docker_latest: 45 | stage: docker_latest 46 | image: docker:19.03.12 47 | services: 48 | - docker:19.03.12-dind 49 | only: 50 | refs: 51 | - dev 52 | script: 53 | - docker build . -t boi12321/porn-vault:edge -f docker/Dockerfile.debian 54 | - docker login -u boi12321 -p $DOCKER_TOKEN 55 | - docker push boi12321/porn-vault:edge 56 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = "test"; 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": 40, 3 | "lines": 50, 4 | "functions": 50, 5 | "statements": 50 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "es5", 4 | singleQuote: false, 5 | printWidth: 100, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /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/config/dev.js: -------------------------------------------------------------------------------- 1 | const endpoints = ["/api", "/assets", "/previews", "/dvd-renderer", "/flag"]; 2 | 3 | const proxy = endpoints.reduce((prox, endpoint) => { 4 | prox[endpoint] = { 5 | target: "http://localhost:3000", 6 | }; 7 | return prox; 8 | }, {}); 9 | 10 | module.exports = (config) => { 11 | config.devServer.proxy(proxy); 12 | }; 13 | -------------------------------------------------------------------------------- /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>PV 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 authLink = setContext((_, { headers }) => { 7 | return { 8 | headers: { 9 | ...headers, 10 | "X-PASS": localStorage.getItem("password"), 11 | }, 12 | }; 13 | }); 14 | 15 | export default new ApolloClient({ 16 | link: authLink.concat(createUploadLink({ uri: "/api/ql" })), 17 | cache: new InMemoryCache(), 18 | defaultOptions: { 19 | watchQuery: { fetchPolicy: "no-cache", errorPolicy: "ignore" }, 20 | query: { fetchPolicy: "no-cache", errorPolicy: "all" }, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /app/src/components/BindFavicon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /app/src/components/BindTitle.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /app/src/components/Cards/Image.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | -------------------------------------------------------------------------------- /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/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 | color 16 | } 17 | frontCover { 18 | _id 19 | color 20 | meta { 21 | dimensions { 22 | width 23 | height 24 | } 25 | } 26 | } 27 | backCover { 28 | _id 29 | meta { 30 | dimensions { 31 | width 32 | height 33 | } 34 | } 35 | } 36 | spineCover { 37 | _id 38 | meta { 39 | dimensions { 40 | width 41 | height 42 | } 43 | } 44 | } 45 | studio { 46 | _id 47 | name 48 | } 49 | duration 50 | size 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /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 | color 21 | } 22 | thumbnail { 23 | _id 24 | color 25 | } 26 | meta { 27 | size 28 | duration 29 | fps 30 | bitrate 31 | dimensions { 32 | width 33 | height 34 | } 35 | } 36 | watches 37 | streamLinks 38 | path 39 | customFields 40 | availableFields { 41 | _id 42 | name 43 | type 44 | values 45 | unit 46 | } 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /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 | url 9 | aliases 10 | rating 11 | favorite 12 | bookmark 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /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/image.ts: -------------------------------------------------------------------------------- 1 | import { VuexModule, Module, Mutation } from "vuex-class-modules"; 2 | 3 | @Module 4 | class ImageModule extends VuexModule {} 5 | 6 | import store from "./index"; 7 | export const imageModule = new ImageModule({ store, name: "images" }); 8 | -------------------------------------------------------------------------------- /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: number; 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 | color?: string; 9 | } 10 | -------------------------------------------------------------------------------- /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 | meta: { 17 | dimensions: { 18 | width: number; 19 | height: number; 20 | }; 21 | }; 22 | } | null; 23 | backCover: { 24 | _id: string; 25 | meta: { 26 | dimensions: { 27 | width: number; 28 | height: number; 29 | }; 30 | }; 31 | } | null; 32 | spineCover: { 33 | _id: string; 34 | meta: { 35 | dimensions: { 36 | width: number; 37 | height: number; 38 | }; 39 | }; 40 | } | null; 41 | studio: any; 42 | scenes: IScene[]; 43 | actors: IActor[]; 44 | labels: { 45 | _id: string; 46 | name: string; 47 | }[]; 48 | 49 | duration: number | null; 50 | size: number | null; 51 | } 52 | -------------------------------------------------------------------------------- /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 | return 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 | return 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/src/views/Settings/Metadata.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /app/src/views/Settings/Settings.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /app/src/views/Settings/System.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /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 | "resize-observer-browser" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /app/vue.config.js: -------------------------------------------------------------------------------- 1 | const applyDev = require('./config/dev') 2 | 3 | module.exports = { 4 | transpileDependencies: ["vuetify"], 5 | chainWebpack: (config) => { 6 | if (process.env.NODE_ENV === "development") { 7 | applyDev(config); 8 | } 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /assets/broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/assets/broken.png -------------------------------------------------------------------------------- /assets/bump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/assets/bump.jpg -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/assets/favicon.png -------------------------------------------------------------------------------- /assets/favicon_mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/assets/favicon_mono.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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /assets/version.json: -------------------------------------------------------------------------------- 1 | { "version": "0.28.0-rc.0" } 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: yes # [yes :: must have a head report to post] 7 | branches: # branch names that can post comment 8 | - "master" 9 | - "dev" 10 | -------------------------------------------------------------------------------- /doc/img/actor_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/actor_collection.jpg -------------------------------------------------------------------------------- /doc/img/actor_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/actor_details.jpg -------------------------------------------------------------------------------- /doc/img/btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/btc.png -------------------------------------------------------------------------------- /doc/img/image_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/image_collection.jpg -------------------------------------------------------------------------------- /doc/img/image_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/image_details.jpg -------------------------------------------------------------------------------- /doc/img/movie_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/movie_collection.jpg -------------------------------------------------------------------------------- /doc/img/movie_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/movie_details.jpg -------------------------------------------------------------------------------- /doc/img/parent_studio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/parent_studio.jpg -------------------------------------------------------------------------------- /doc/img/scene_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/scene_collection.jpg -------------------------------------------------------------------------------- /doc/img/scene_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/scene_details.jpg -------------------------------------------------------------------------------- /doc/img/studio_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/doc/img/studio_collection.jpg -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | # This Dockerfile requires the build context to be the repository root 2 | # and not it's own folder 3 | 4 | FROM node:14-buster as build-env 5 | 6 | WORKDIR /app 7 | ADD . /app 8 | 9 | RUN cd /app && \ 10 | npm ci && \ 11 | npm run install:app && \ 12 | npm run build:generic 13 | 14 | FROM ghcr.io/linuxserver/baseimage-ubuntu:focal 15 | 16 | RUN apt-get update && \ 17 | apt-get -y install ca-certificates ffmpeg imagemagick && \ 18 | rm -rf /var/lib/apt/lists/* 19 | RUN sed -i 's/name="width" value="16KP"/name="width" value="32KP"/' /etc/ImageMagick-6/policy.xml && sed -i 's/name="height" value="16KP"/name="height" value="32KP"/' /etc/ImageMagick-6/policy.xml 20 | 21 | COPY --from=build-env /app/releases/node14 / 22 | COPY ["config.example.json", "./docker/root/", "/"] 23 | 24 | VOLUME [ "/config"] 25 | EXPOSE 3000 26 | ENV PV_CONFIG_FOLDER=/config 27 | -------------------------------------------------------------------------------- /docker/root/etc/cont-init.d/40-init-dirs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # We want a configuration file in /config so it can be stored in a persistent volume and not wiped out if we update the container. 4 | if [ ! -f /config/config.json ] && [ ! -f /config/config.yaml ] 5 | then 6 | echo "copying example configuration to /config/config.json" 7 | cp config.example.json /config/config.json 8 | fi 9 | 10 | # set permissions non-recursively on config folders 11 | chown abc:abc /config /config/* -------------------------------------------------------------------------------- /docker/root/etc/cont-init.d/50-gid-video: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | FILES=$(find /dev/dri /dev/dvb -type c -print 2>/dev/null) 4 | 5 | for i in $FILES 6 | do 7 | VIDEO_GID=$(stat -c '%g' "$i") 8 | if id -G abc | grep -qw "$VIDEO_GID"; then 9 | touch /groupadd 10 | else 11 | if [ ! "${VIDEO_GID}" == '0' ]; then 12 | VIDEO_NAME=$(getent group "${VIDEO_GID}" | awk -F: '{print $1}') 13 | if [ -z "${VIDEO_NAME}" ]; then 14 | VIDEO_NAME="video$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c8)" 15 | groupadd "$VIDEO_NAME" 16 | groupmod -g "$VIDEO_GID" "$VIDEO_NAME" 17 | fi 18 | usermod -a -G "$VIDEO_NAME" abc 19 | touch /groupadd 20 | fi 21 | fi 22 | done 23 | 24 | if [ -n "${FILES}" ] && [ ! -f "/groupadd" ]; then 25 | usermod -a -G root abc 26 | fi -------------------------------------------------------------------------------- /docker/root/etc/services.d/porn-vault/finish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/execlineb -S1 2 | if { s6-test ${1} -ne 0 } 3 | if { s6-test ${1} -ne 256 } 4 | 5 | s6-svscanctl -t /var/run/s6/services -------------------------------------------------------------------------------- /docker/root/etc/services.d/porn-vault/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | echo "Starting Porn Vault" 4 | 5 | cd / 6 | exec s6-setuidgid abc /porn-vault -------------------------------------------------------------------------------- /imagemagick.md: -------------------------------------------------------------------------------- 1 | - Go to installation path of imagemagick 2 | 3 | - Open policy.xml 4 | 5 | Change 6 | 7 | ``` 8 | 9 | 10 | ``` 11 | 12 | to 13 | 14 | ``` 15 | 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["plugins/*", "assets/*", "config.json", "logs/*"] 3 | } 4 | -------------------------------------------------------------------------------- /src/args.ts: -------------------------------------------------------------------------------- 1 | import yargs from "yargs"; 2 | 3 | import VERSION from "./version"; 4 | 5 | const argv = yargs 6 | .version(VERSION) 7 | .option("process-queue", { 8 | type: "boolean", 9 | description: "(Used internally, don't use manually)", 10 | default: false, 11 | }) 12 | .option("skip-compaction", { 13 | type: "boolean", 14 | description: "Skip database file compaction (decreases startup time)", 15 | default: false, 16 | }) 17 | .option("update-izzy", { 18 | type: "boolean", 19 | description: "Remove database binary and download latest version", 20 | default: false, 21 | }) 22 | .option("generate-image-thumbnails", { 23 | type: "boolean", 24 | description: "Generate all image thumbnails", 25 | default: false, 26 | }) 27 | .option("reindex", { 28 | type: "boolean", 29 | description: "Delete search indices and rebuild", 30 | default: false, 31 | }) 32 | .option("reset-izzy", { 33 | type: "boolean", 34 | description: "Reload database from files", 35 | default: false, 36 | }).argv; 37 | 38 | export default argv; 39 | -------------------------------------------------------------------------------- /src/binaries/imagemagick.ts: -------------------------------------------------------------------------------- 1 | import execa from "execa"; 2 | 3 | import { getConfig } from "../config"; 4 | 5 | export function getImageDimensions(input: string): { width: number; height: number } { 6 | const proc = execa.sync(getConfig().imagemagick.identifyPath, ["-format", "%[w] %[h]", input]); 7 | const dims = proc.stdout 8 | .trim() 9 | .split(" ") 10 | .map((x) => parseInt(x)); 11 | return { width: dims[0], height: dims[1] }; 12 | } 13 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { existsSync } from "fs"; 3 | 4 | import { logger } from "./utils/logger"; 5 | 6 | export function loadEnv(file = ".env") { 7 | if (existsSync(file)) { 8 | logger.debug(`Loading ${file}`); 9 | dotenv.config({ 10 | path: file, 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/exit.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "./utils/logger"; 2 | 3 | function killProcess(code = 0) { 4 | return () => { 5 | // When running tests, we want to be able to cleanup any services, 6 | // but we cannot overload the actual 'exit' otherwise mocha's 7 | // exit code will not reflect the actual result of the tests 8 | if (process.env.NODE_ENV !== "test") { 9 | logger.debug(`Closing with code ${code}`); 10 | process.exit(code); 11 | } 12 | }; 13 | } 14 | 15 | export function applyExitHooks(): void { 16 | logger.debug("Apply exit hooks"); 17 | process.on("exit", killProcess(0)); 18 | process.on("SIGTERM", killProcess(0)); 19 | process.on("SIGINT", killProcess(0)); 20 | process.on("SIGUSR1", killProcess(0)); 21 | process.on("SIGUSR2", killProcess(0)); 22 | process.on("uncaughtException", (e) => { 23 | console.log("Uncaught Exception..."); 24 | console.log(e.stack); 25 | killProcess(99)(); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /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/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 GraphQLJSON, { GraphQLJSONObject } from "graphql-type-json"; 2 | import GraphQLLong from "graphql-type-long"; 3 | import { GraphQLUpload } from "graphql-upload"; 4 | 5 | import MutationResolver from "./mutation"; 6 | import ActorResolver from "./resolvers/actor"; 7 | import CustomFieldResolver from "./resolvers/custom_field"; 8 | import ImageResolver from "./resolvers/image"; 9 | import LabelResolver from "./resolvers/label"; 10 | import MarkerResolver from "./resolvers/marker"; 11 | import MovieResolver from "./resolvers/movie"; 12 | import QueryResolvers from "./resolvers/query"; 13 | import SceneResolver from "./resolvers/scene"; 14 | import SceneViewResolver from "./resolvers/scene_view"; 15 | import StudioResolver from "./resolvers/studio"; 16 | 17 | const resolvers = { 18 | Upload: GraphQLUpload, 19 | 20 | Long: GraphQLLong, 21 | Object: GraphQLJSONObject, 22 | Json: GraphQLJSON, 23 | 24 | Actor: ActorResolver, 25 | Scene: SceneResolver, 26 | Image: ImageResolver, 27 | Query: QueryResolvers, 28 | Mutation: MutationResolver, 29 | Label: LabelResolver, 30 | Movie: MovieResolver, 31 | Studio: StudioResolver, 32 | CustomField: CustomFieldResolver, 33 | Marker: MarkerResolver, 34 | SceneView: SceneViewResolver, 35 | }; 36 | 37 | export default resolvers; 38 | -------------------------------------------------------------------------------- /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 | const actors = await Image.getActors(image); 10 | return actors.sort((a, b) => a.name.localeCompare(b.name)); 11 | }, 12 | async scene(image: Image): Promise { 13 | if (image.scene) return await Scene.getById(image.scene); 14 | return null; 15 | }, 16 | async labels(image: Image): Promise { 17 | const labels = await Image.getLabels(image); 18 | return labels.sort((a, b) => a.name.localeCompare(b.name)); 19 | }, 20 | async studio(image: Image): Promise { 21 | if (image.studio) return Studio.getById(image.studio); 22 | return null; 23 | }, 24 | color(image: Image): string | null { 25 | return Image.color(image) || null; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /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 Actor from "../../types/actor"; 2 | import Image from "../../types/image"; 3 | import Label from "../../types/label"; 4 | import Marker from "../../types/marker"; 5 | import Scene from "../../types/scene"; 6 | 7 | export default { 8 | async actors(marker: Marker): Promise { 9 | const actors = await Marker.getActors(marker); 10 | return actors.sort((a, b) => a.name.localeCompare(b.name)); 11 | }, 12 | async labels(marker: Marker): Promise { 13 | const labels = await Marker.getLabels(marker); 14 | return labels.sort((a, b) => a.name.localeCompare(b.name)); 15 | }, 16 | async thumbnail(marker: Marker): Promise { 17 | if (marker.thumbnail) return await Image.getById(marker.thumbnail); 18 | return null; 19 | }, 20 | async scene(marker: Marker): Promise { 21 | return Scene.getById(marker.scene); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /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 { collections } from "../../../database"; 2 | import { IImageSearchQuery, searchImages } from "../../../search/image"; 3 | import Image from "../../../types/image"; 4 | import { logger } from "../../../utils/logger"; 5 | 6 | export async function getImages( 7 | _: unknown, 8 | { query, seed }: { query: Partial; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: Image[]; 14 | } 15 | | undefined 16 | > { 17 | const timeNow = +new Date(); 18 | 19 | const result = await searchImages(query, seed); 20 | logger.verbose(`Search results: ${result.total} hits found in ${(Date.now() - timeNow) / 1000}s`); 21 | 22 | const scenes = await collections.images.getBulk(result.items); 23 | logger.verbose(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 24 | 25 | return { 26 | numItems: result.total, 27 | numPages: result.numPages, 28 | items: scenes, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/marker.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "../../../database"; 2 | import { IMarkerSearchQuery, searchMarkers } from "../../../search/marker"; 3 | import Marker from "../../../types/marker"; 4 | import { logger } from "../../../utils/logger"; 5 | 6 | export async function getMarkers( 7 | _: unknown, 8 | { query, seed }: { query: Partial; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: (Marker | null)[]; 14 | } 15 | | undefined 16 | > { 17 | const timeNow = +new Date(); 18 | 19 | const result = await searchMarkers(query, seed); 20 | logger.verbose(`Search results: ${result.total} hits found in ${(Date.now() - timeNow) / 1000}s`); 21 | 22 | const scenes = await collections.markers.getBulk(result.items); 23 | logger.verbose(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 24 | 25 | return { 26 | numItems: result.total, 27 | numPages: result.numPages, 28 | items: scenes, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/movie.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "../../../database"; 2 | import { IMovieSearchQuery, searchMovies } from "../../../search/movie"; 3 | import Movie from "../../../types/movie"; 4 | import { logger } from "../../../utils/logger"; 5 | 6 | export async function getMovies( 7 | _: unknown, 8 | { query, seed }: { query: Partial; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: Movie[]; 14 | } 15 | | undefined 16 | > { 17 | const timeNow = +new Date(); 18 | 19 | const result = await searchMovies(query, seed); 20 | logger.verbose(`Search results: ${result.total} hits found in ${(Date.now() - timeNow) / 1000}s`); 21 | 22 | const scenes = await collections.movies.getBulk(result.items); 23 | logger.verbose(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 24 | 25 | return { 26 | numItems: result.total, 27 | numPages: result.numPages, 28 | items: scenes, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/scene.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "../../../database"; 2 | import { ISceneSearchQuery, searchScenes } from "../../../search/scene"; 3 | import Scene from "../../../types/scene"; 4 | import { logger } from "../../../utils/logger"; 5 | 6 | export async function getScenes( 7 | _: unknown, 8 | { query, seed }: { query: Partial; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: Scene[]; 14 | } 15 | | undefined 16 | > { 17 | const timeNow = +new Date(); 18 | 19 | const result = await searchScenes(query, seed); 20 | logger.verbose(`Search results: ${result.total} hits found in ${(Date.now() - timeNow) / 1000}s`); 21 | 22 | const scenes = await collections.scenes.getBulk(result.items); 23 | logger.verbose(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 24 | 25 | return { 26 | numItems: result.total, 27 | numPages: result.numPages, 28 | items: scenes, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/resolvers/search/studio.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "../../../database"; 2 | import { IStudioSearchQuery, searchStudios } from "../../../search/studio"; 3 | import Studio from "../../../types/studio"; 4 | import { logger } from "../../../utils/logger"; 5 | 6 | export async function getStudios( 7 | _: unknown, 8 | { query, seed }: { query: Partial; seed?: string } 9 | ): Promise< 10 | | { 11 | numItems: number; 12 | numPages: number; 13 | items: Studio[]; 14 | } 15 | | undefined 16 | > { 17 | const timeNow = +new Date(); 18 | 19 | const result = await searchStudios(query, seed); 20 | logger.verbose(`Search results: ${result.total} hits found in ${(Date.now() - timeNow) / 1000}s`); 21 | 22 | const scenes = await collections.studios.getBulk(result.items); 23 | logger.verbose(`Search done in ${(Date.now() - timeNow) / 1000}s.`); 24 | 25 | return { 26 | numItems: result.total, 27 | numPages: result.numPages, 28 | items: scenes, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql/schema/custom_field.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server-express"; 2 | 3 | export default gql` 4 | extend type Query { 5 | getCustomFields(target: CustomFieldTarget): [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 | scalar Json 8 | 9 | type SceneView { 10 | _id: String! 11 | scene: Scene 12 | date: Long! 13 | } 14 | 15 | type Query { 16 | getQueueInfo: QueueInfo! 17 | getWatches(min: Long, max: Long): [SceneView!]! 18 | } 19 | 20 | type Mutation { 21 | attachLabels(item: String!, labels: [String!]!): Boolean! 22 | removeLabel(item: String!, label: String!): Boolean! 23 | } 24 | 25 | input Crop { 26 | left: Int! 27 | top: Int! 28 | width: Int! 29 | height: Int! 30 | } 31 | 32 | type QueueInfo { 33 | length: Int! 34 | processing: Boolean! 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /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: [Label!]! 7 | getLabelById(id: String!): Label 8 | } 9 | 10 | type Label { 11 | _id: String! 12 | name: String! 13 | aliases: [String!]! 14 | addedOn: Long! 15 | color: String 16 | 17 | # Resolvers 18 | thumbnail: Image 19 | } 20 | 21 | input LabelUpdateOpts { 22 | name: String 23 | aliases: [String!] 24 | thumbnail: String 25 | color: String 26 | } 27 | 28 | extend type Mutation { 29 | addLabel(name: String!, aliases: [String!]): Label! 30 | updateLabels(ids: [String!]!, opts: LabelUpdateOpts!): [Label!]! 31 | removeLabels(ids: [String!]!): Boolean! 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /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: "warn", 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { startup } from "./startup"; 2 | import { logger } from "./utils/logger"; 3 | 4 | (async (): Promise => { 5 | await startup(); 6 | })().catch((err: Error) => { 7 | logger.error(err.message); 8 | logger.debug(err.stack); 9 | process.exit(1); 10 | }); 11 | -------------------------------------------------------------------------------- /src/middlewares/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server-express"; 2 | import { ApolloServerPlugin, GraphQLRequestListener } from "apollo-server-plugin-base"; 3 | import express from "express"; 4 | import { graphqlUploadExpress } from "graphql-upload"; 5 | 6 | import schema from "../graphql/types"; 7 | import { formatMessage, logger } from "../utils/logger"; 8 | 9 | const apolloLogger: ApolloServerPlugin = { 10 | requestDidStart(requestContext): GraphQLRequestListener { 11 | return { 12 | didEncounterErrors(requestContext) { 13 | logger.error(`Error in graphql api: ${formatMessage(requestContext.errors)}`); 14 | }, 15 | }; 16 | }, 17 | }; 18 | 19 | export function mountApolloServer(app: express.Application): void { 20 | const server = new ApolloServer({ 21 | schema, 22 | context: ({ req }) => ({ 23 | req, 24 | }), 25 | uploads: false, 26 | playground: !!process.env.PV_QL_PLAYGROUND, 27 | plugins: [apolloLogger], 28 | }); 29 | app.use(graphqlUploadExpress()); 30 | server.applyMiddleware({ app, path: "/api/ql" }); 31 | } 32 | -------------------------------------------------------------------------------- /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/plugins/store.ts: -------------------------------------------------------------------------------- 1 | const store: Record = {}; 2 | 3 | function storeHasItem(key: string): boolean { 4 | return key in store; 5 | } 6 | 7 | function getStoreItem(key: string): unknown { 8 | return store[key]; 9 | } 10 | 11 | function setStoreItem(key: string, value: T): void { 12 | store[key] = value; 13 | } 14 | 15 | function removeStoreItem(key: string): void { 16 | delete store[key]; 17 | } 18 | 19 | export function createPluginStoreAccess(pluginName: string) { 20 | function getNamespacedKey(key: string) { 21 | return `${pluginName}-${key}`; 22 | } 23 | 24 | return { 25 | setItem: (key: string, value: unknown) => setStoreItem(getNamespacedKey(key), value), 26 | getItem: (key: string) => getStoreItem(getNamespacedKey(key)), 27 | hasItem: (key: string) => storeHasItem(getNamespacedKey(key)), 28 | removeItem: (key: string) => removeStoreItem(getNamespacedKey(key)), 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/types.ts: -------------------------------------------------------------------------------- 1 | export type PluginEvents = 2 | | "actorCreated" 3 | | "actorCustom" 4 | | "sceneCreated" 5 | | "sceneCustom" 6 | | "movieCreated"; 7 | 8 | export interface PluginArg { 9 | name: string; 10 | type: boolean; 11 | required: boolean; 12 | default?: any; 13 | description?: string; 14 | } 15 | 16 | export interface IPluginInfo { 17 | // Taken from plugin's info.json 18 | events: PluginEvents[]; 19 | arguments: PluginArg[]; 20 | version: string; 21 | authors: string[]; 22 | name: string; 23 | description: string; 24 | } 25 | 26 | export type IPluginMetadata = { 27 | // Used to validate usage 28 | requiredVersion: string; 29 | validateArguments: (args: unknown) => boolean; 30 | } & { info: IPluginInfo }; 31 | 32 | export type PluginFunction = (ctx: Input) => Promise; 33 | export type Plugin = PluginFunction & Partial; 34 | 35 | export type UnknownPlugin = Plugin; 36 | -------------------------------------------------------------------------------- /src/routers/config.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { configFile, getConfig } from "../config"; 4 | 5 | const router = Router(); 6 | 7 | router.get("/", (req, res) => { 8 | res.json({ 9 | location: configFile, 10 | value: getConfig(), 11 | }); 12 | }); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/routers/scan.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { getConfig } from "../config"; 4 | import { isScanning, nextScanTimestamp, scanFolders } from "../scanner"; 5 | import { handleError } from "../utils/logger"; 6 | 7 | const router = Router(); 8 | 9 | router.get("/folders", (req, res) => { 10 | const { images, videos } = getConfig().import; 11 | res.json({ 12 | images, 13 | videos, 14 | amount: images.length + videos.length, 15 | }); 16 | }); 17 | 18 | router.post("/", (req, res) => { 19 | if (isScanning) { 20 | res.status(409).json("Scan already in progress"); 21 | } else { 22 | const config = getConfig(); 23 | scanFolders(config.scan.interval).catch((err: Error) => { 24 | handleError("Error starting scan: ", err); 25 | }); 26 | res.json("Started scan."); 27 | } 28 | }); 29 | 30 | router.get("/", (req, res) => { 31 | res.json({ 32 | isScanning, 33 | nextScanDate: nextScanTimestamp ? new Date(nextScanTimestamp).toLocaleString() : null, 34 | nextScanTimestamp, 35 | }); 36 | }); 37 | 38 | export default router; 39 | -------------------------------------------------------------------------------- /src/routers/static.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from "express"; 2 | 3 | export function applyFlagRoute(app: Application): void { 4 | app.get("/flag/:code", (req, res) => { 5 | res.redirect(`/assets/flags/${req.params.code.toLowerCase()}.svg`); 6 | }); 7 | } 8 | 9 | export function applyStaticRoutes(app: Application): void { 10 | app.use("/js", express.static("./app/dist/js")); 11 | app.use("/css", express.static("./app/dist/css")); 12 | app.use("/fonts", express.static("./app/dist/fonts")); 13 | app.use("/previews", express.static("./library/previews")); 14 | app.use("/assets", express.static("./assets")); 15 | } 16 | -------------------------------------------------------------------------------- /src/search/internal/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_RESULT = 2500000; 2 | -------------------------------------------------------------------------------- /src/static.ts: -------------------------------------------------------------------------------- 1 | import express, { Application } from "express"; 2 | 3 | import { logger } from "./utils/logger"; 4 | 5 | export function applyPublic(app: Application) { 6 | logger.debug("Applying public static routes"); 7 | app.use("/js", express.static("./app/dist/js")); 8 | app.use("/css", express.static("./app/dist/css")); 9 | app.use("/fonts", express.static("./app/dist/fonts")); 10 | app.use("/previews", express.static("./library/previews")); 11 | app.use("/assets", express.static("./assets")); 12 | app.get("/flag/:code", (req, res) => { 13 | res.redirect(`/assets/flags/${req.params.code.toLowerCase()}.svg`); 14 | }); 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/watch.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "../database"; 2 | import { generateHash } from "../utils/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 collections.views.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 collections.views.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/utils/async.ts: -------------------------------------------------------------------------------- 1 | export function mapAsync( 2 | array: T[], 3 | callbackfn: (value: T, index: number, array: T[]) => U | Promise 4 | ): Promise { 5 | return Promise.all(array.map(callbackfn)); 6 | } 7 | 8 | export async function filterAsync( 9 | array: T[], 10 | callbackfn: (value: T, index: number, array: T[]) => boolean | Promise 11 | ): Promise { 12 | const filterMap = await mapAsync(array, callbackfn); 13 | return array.filter((_value, index) => filterMap[index]); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/fs/index.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync } from "fs"; 2 | 3 | export const isDirectory = (path: string): boolean => lstatSync(path).isDirectory(); 4 | -------------------------------------------------------------------------------- /src/utils/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/utils/http.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from "../config/schema"; 2 | 3 | export function protocol(config: IConfig) { 4 | return config.server.https.enable ? "https" : "http"; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/mem.ts: -------------------------------------------------------------------------------- 1 | import v8 from "v8"; 2 | 3 | import { logger } from "./logger"; 4 | 5 | export function printMaxMemory(): void { 6 | logger.info( 7 | `Max. memory: ${Math.round(v8.getHeapStatistics().total_available_size / 1024 / 1024)} MB` 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import { getConfig } from "../config"; 4 | 5 | export function libraryPath(str: string): string { 6 | return path.join(getConfig().persistence.libraryPath, "library", str); 7 | } 8 | 9 | const configFolder = process.env.PV_CONFIG_FOLDER || process.cwd(); 10 | 11 | export function configPath(...paths: string[]): string { 12 | return path.resolve(path.join(configFolder, ...paths)); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/render.ts: -------------------------------------------------------------------------------- 1 | import Handlebars from "handlebars"; 2 | 3 | import { readFileAsync } from "../utils/fs/async"; 4 | 5 | export async function renderHandlebars(file: string, context: T): Promise { 6 | const text = await readFileAsync(file, "utf-8"); 7 | return Handlebars.compile(text)(context); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | const EXTENSION_REGEX = /(\.[^/.\s]+)$/; 2 | 3 | export function isHexColor(str: string): boolean { 4 | return /^#[a-f0-9]{6}$/i.test(str); 5 | } 6 | 7 | export function getExtension(file: string): string { 8 | return EXTENSION_REGEX.exec(file)?.[0] || ""; 9 | } 10 | 11 | export function extensionFromUrl(url: string): string { 12 | const clean = url.split("?")[0].split("#")[0]; 13 | return getExtension(clean) || ""; 14 | } 15 | 16 | export function removeExtension(file: string): string { 17 | return file.replace(EXTENSION_REGEX, ""); 18 | } 19 | 20 | /** 21 | * @param str - the string to strip 22 | * @returns the string without diacritics 23 | */ 24 | export const stripAccents = (str: string): string => 25 | str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 26 | 27 | /** 28 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping 29 | * 30 | * @param string - input string 31 | */ 32 | export function escapeRegExp(string: string): string { 33 | return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type Dictionary = Record; 2 | 3 | export function isNumber(i: unknown): i is number { 4 | return typeof i === "number"; 5 | } 6 | 7 | export function isBoolean(i: unknown): i is boolean { 8 | return typeof i === "boolean"; 9 | } 10 | export function isRegExp(regStr: string): boolean { 11 | try { 12 | // eslint-disable-next-line no-new 13 | new RegExp(regStr); 14 | return true; 15 | } catch (e) { 16 | return false; 17 | } 18 | } 19 | 20 | export type DeepPartial = { 21 | [P in keyof T]?: DeepPartial; 22 | }; 23 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | // eslint-disable-next-line 3 | export default require(resolve("./assets/version.json")).version as string; 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.isNotTrue(validationResult); 24 | assert.isObject(validationResult); 25 | assert.instanceOf((validationResult as any).error, Error); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/fixtures/files/.hidden/image100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/.hidden/image100.jpg -------------------------------------------------------------------------------- /test/fixtures/files/dynamic/.gitkeep: -------------------------------------------------------------------------------- 1 | Keep this folder in git so tests don't have to create it -------------------------------------------------------------------------------- /test/fixtures/files/image001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/image001.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/image002.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/image003.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/image004.jpg -------------------------------------------------------------------------------- /test/fixtures/files/image005.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/image005.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image006.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/some_folder/image006.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image007.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/some_folder/image007.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image008.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/some_folder/image008.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image009.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/some_folder/image009.jpg -------------------------------------------------------------------------------- /test/fixtures/files/some_folder/image010.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaginessa/porn-vault/4b31a8b3f7ef66204ebb1cc0769dcf1842c10a23/test/fixtures/files/some_folder/image010.jpg -------------------------------------------------------------------------------- /test/fixtures/files/video001.mp4: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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/matching/fixtures/string_filter.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: "should return input order", 4 | options:{ 5 | ignoreSingleNames:false, 6 | sortByLongestMatch: false, 7 | }, 8 | items: [ 9 | { 10 | _id: "short", 11 | name: "Gina", 12 | }, 13 | { 14 | _id: "long", 15 | name: "Gina Valentina", 16 | }, 17 | ], 18 | str: 19 | "Sloppy.Gargling.Suck.Party.Gina.Valentina.&.Jill.Kassidy.Swallowed.mp4", 20 | expected: ["Gina", "Gina Valentina"], 21 | }, 22 | { 23 | name: "should return by longest match order", 24 | options:{ 25 | ignoreSingleNames:false, 26 | sortByLongestMatch: true, 27 | }, 28 | items: [ 29 | { 30 | _id: "short", 31 | name: "Gina", 32 | }, 33 | { 34 | _id: "long", 35 | name: "Gina Valentina", 36 | }, 37 | ], 38 | str: 39 | "Sloppy.Gargling.Suck.Party.Gina.Valentina.&.Jill.Kassidy.Swallowed.mp4", 40 | expected: ["Gina Valentina", "Gina"], 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /test/matching/fixtures/strip_string.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | source: "original string", 4 | expected: "originalstring", 5 | }, 6 | { 7 | source: 8 | "Jill.And.Gina.In.A.Sloppy.Gargling.Suck.Party.Gina.Valentina.&.Jill.Kassidy.Swallowed.mp4", 9 | expected: "jillandginainasloppygarglingsuckpartyginavalentinajillkassidyswallowedmp4", 10 | }, 11 | { 12 | source: "/data/paige owens.mp4", 13 | expected: "/data/paigeowensmp4", 14 | }, 15 | { 16 | source: 17 | "Jill And Gina In A Sloppy Gargling Suck Party - gina Valentina&jill Kassidy - Swallowed [blowjob, threesome]", 18 | expected: 19 | "jillandginainasloppygarglingsuckparty-ginavalentinajillkassidy-swallowed[blowjob,threesome]", 20 | }, 21 | { 22 | source: 23 | "Jill And Gina In A Sloppy Gargling Suck Party - gina Valentina&jill Kassidy - Swallowed (blowjob, threesome)", 24 | expected: 25 | "jillandginainasloppygarglingsuckparty-ginavalentinajillkassidy-swallowed(blowjob,threesome)", 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /test/matching/matcher.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { ignoreSingleNames, isSingleWord } from "../../src/matching/matcher"; 4 | 5 | describe("matcher", () => { 6 | describe("isSingleWord", () => { 7 | for (const word of ["test", "", "1234", "numbermix1234"]) 8 | it("Should be a seen as a single word", () => { 9 | expect(isSingleWord(word)).to.be.true; 10 | }); 11 | 12 | for (const word of [ 13 | "avi love", 14 | "some fairly long sentence", 15 | "even works with 124124 numbers", 16 | ]) { 17 | it("Should not be a seen as a single word", () => { 18 | expect(isSingleWord(word)).to.be.false; 19 | }); 20 | } 21 | }); 22 | 23 | describe("ignoreSingleNames", () => { 24 | it("Should ignore single names", () => { 25 | expect(ignoreSingleNames(["", "name", "avi love", "kali roses"])).deep.equals([ 26 | "avi love", 27 | "kali roses", 28 | ]); 29 | }); 30 | 31 | it("Should not ignore regex", () => { 32 | expect(ignoreSingleNames(["regex:(avi love)|(avi looove)", "regex:[a-z]+"])).deep.equals([ 33 | "regex:(avi love)|(avi looove)", 34 | "regex:[a-z]+", 35 | ]); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/plugins/fixtures/actor_plugin.fixture.js: -------------------------------------------------------------------------------- 1 | const mockActor = { 2 | name: "mock actor name", 3 | description: "mock actor description", 4 | // Use a constant date, so individual imports will have same date 5 | bornOn: new Date("2020-10-09T07:49:52.636Z").valueOf(), 6 | aliases: ["mock actor alias"], 7 | rating: 5, 8 | favorite: true, 9 | bookmark: 1, 10 | nationality: "US", 11 | labels: ["existing actor label"], 12 | }; 13 | 14 | const plugin = async ({ $createLocalImage, $createImage }) => { 15 | // Create existing image 16 | const existingImage = await $createLocalImage( 17 | "test/fixtures/files/image001.jpg", 18 | mockActor.name + " image001", 19 | false 20 | ); 21 | 22 | // Create extra image for the gallery 23 | await $createImage( 24 | "https://picsum.photos/seed/picsum/400/400.jpg", 25 | mockActor.name + " image001", 26 | false 27 | ); 28 | 29 | return { 30 | ...mockActor, 31 | thumbnail: await $createImage( 32 | "https://picsum.photos/seed/picsum/200/300.jpg", 33 | mockActor.name + " thumbnail", 34 | true 35 | ), 36 | existingImage, 37 | }; 38 | }; 39 | 40 | // Attach the result to the exported plugin 41 | // so tests can use it to compare the result 42 | plugin.result = mockActor; 43 | 44 | module.exports = plugin; 45 | -------------------------------------------------------------------------------- /test/plugins/fixtures/actor_plugin.fixture.ts: -------------------------------------------------------------------------------- 1 | const plugin = async (ctx) => { 2 | return await require("./actor_plugin.fixture.js")(ctx); 3 | }; 4 | 5 | plugin.result = require("./actor_plugin.fixture.js").result; 6 | 7 | module.exports = plugin; 8 | -------------------------------------------------------------------------------- /test/plugins/fixtures/movie_plugin.fixture.js: -------------------------------------------------------------------------------- 1 | const mockMovie = { 2 | name: "mock movie name", 3 | description: "mock movie description", 4 | // Use a constant date, so individual imports will have same date 5 | releaseDate: new Date("2020-10-09T07:49:52.636Z").valueOf(), 6 | rating: 5, 7 | favorite: true, 8 | bookmark: 1, 9 | }; 10 | 11 | const plugin = async ({ $createLocalImage }) => { 12 | // Create existing image 13 | const existingImage = await $createLocalImage( 14 | "test/fixtures/files/image001.jpg", 15 | mockMovie.name + " image001", 16 | false 17 | ); 18 | 19 | return { 20 | ...mockMovie, 21 | existingImage, 22 | }; 23 | }; 24 | 25 | // Attach the result to the exported plugin 26 | // so tests can use it to compare the result 27 | plugin.result = mockMovie; 28 | 29 | module.exports = plugin; 30 | -------------------------------------------------------------------------------- /test/plugins/fixtures/movie_plugin.fixture.ts: -------------------------------------------------------------------------------- 1 | const plugin = async () => { 2 | return require("./movie_plugin.fixture.js").result; 3 | }; 4 | 5 | plugin.result = require("./movie_plugin.fixture.js").result; 6 | 7 | module.exports = plugin; 8 | -------------------------------------------------------------------------------- /test/plugins/fixtures/scene_plugin.fixture.js: -------------------------------------------------------------------------------- 1 | const mockScene = { 2 | name: "mock scene name", 3 | // path: "mock scene path", 4 | description: "mock scene description", 5 | // Use a constant date, so individual imports will have same date 6 | releaseDate: new Date("2020-10-09T07:49:52.636Z").valueOf(), 7 | addedOn: new Date("2020-10-09T07:49:52.636Z").valueOf(), 8 | rating: 5, 9 | favorite: true, 10 | actors: ["existing actor name"], 11 | bookmark: 1, 12 | studio: "existing studio", 13 | labels: ["existing scene label"], 14 | }; 15 | 16 | const plugin = async ({ $createLocalImage, $createImage }) => { 17 | // Create existing image 18 | const existingImage = await $createLocalImage( 19 | "test/fixtures/files/image001.jpg", 20 | mockScene.name + " image001", 21 | false 22 | ); 23 | 24 | await $createImage( 25 | "https://picsum.photos/seed/picsum/400/400.jpg", 26 | mockScene.name + " image001", 27 | false 28 | ); 29 | 30 | return { 31 | ...mockScene, 32 | thumbnail: await $createImage( 33 | "https://picsum.photos/seed/picsum/200/300.jpg", 34 | mockScene.name + " thumbnail", 35 | true 36 | ), 37 | existingImage, 38 | }; 39 | }; 40 | 41 | // Attach the result to the exported plugin 42 | // so tests can use it to compare the result 43 | plugin.result = mockScene; 44 | 45 | module.exports = plugin; 46 | -------------------------------------------------------------------------------- /test/plugins/fixtures/scene_plugin.fixture.ts: -------------------------------------------------------------------------------- 1 | const plugin = async (ctx) => { 2 | return await require("./scene_plugin.fixture.js")(ctx); 3 | }; 4 | 5 | plugin.result = require("./scene_plugin.fixture.js").result; 6 | 7 | module.exports = plugin; 8 | -------------------------------------------------------------------------------- /test/plugins/fixtures/studio/recursive.fixture.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync, readFileSync, existsSync } = require("fs"); 2 | 3 | const mocks = { 4 | studio_1: { 5 | name: "studio_1", 6 | parent: "studio_2", 7 | }, 8 | studio_2: { 9 | name: "studio_2", 10 | parent: "studio_3", 11 | }, 12 | studio_3: { 13 | name: "studio_3", 14 | parent: "studio_4", 15 | }, 16 | studio_4: { 17 | name: "studio_4", 18 | parent: "studio_5", 19 | }, 20 | studio_5: { 21 | name: "studio_5", 22 | parent: "studio_6", 23 | }, 24 | studio_6: { 25 | name: "studio_6", 26 | parent: "studio_7", 27 | }, 28 | studio_7: { 29 | name: "studio_8", 30 | parent: "studio_8", 31 | }, 32 | }; 33 | 34 | const plugin = async ({ studioName }) => { 35 | let callCount = 0; 36 | if (existsSync("./test/plugins/fixtures/studio/call_count")) { 37 | callCount = parseInt(readFileSync("./test/plugins/fixtures/studio/call_count", "utf-8")); 38 | } 39 | writeFileSync("./test/plugins/fixtures/studio/call_count", `${callCount + 1}`, "utf-8"); 40 | return mocks[studioName]; 41 | }; 42 | 43 | module.exports = plugin; 44 | -------------------------------------------------------------------------------- /test/plugins/fixtures/studio_plugin.fixture.js: -------------------------------------------------------------------------------- 1 | const mockStudio = { 2 | name: "mock studio name", 3 | description: "mock studio description", 4 | favorite: true, 5 | bookmark: 1, 6 | aliases: ["mock studio alias"], 7 | labels: ["existing studio label"], 8 | }; 9 | 10 | const plugin = async ({ $createLocalImage, $createImage }) => { 11 | // Create existing image 12 | const existingImage = await $createLocalImage( 13 | "test/fixtures/files/image001.jpg", 14 | mockStudio.name + " image001", 15 | false 16 | ); 17 | 18 | await $createImage( 19 | "https://picsum.photos/seed/picsum/400/400.jpg", 20 | mockStudio.name + " image001", 21 | false 22 | ); 23 | 24 | return { 25 | ...mockStudio, 26 | thumbnail: await $createImage( 27 | "https://picsum.photos/seed/picsum/200/300.jpg", 28 | mockStudio.name + " thumbnail", 29 | true 30 | ), 31 | existingImage, 32 | }; 33 | }; 34 | 35 | // Attach the result to the exported pluginn 36 | // so tests can use it to compare the result 37 | plugin.result = mockStudio; 38 | 39 | module.exports = plugin; 40 | -------------------------------------------------------------------------------- /test/plugins/fixtures/studio_plugin.fixture.ts: -------------------------------------------------------------------------------- 1 | const plugin = async (ctx) => { 2 | return await require("./studio_plugin.fixture.js")(ctx); 3 | }; 4 | 5 | plugin.result = require("./studio_plugin.fixture.js").result; 6 | 7 | module.exports = plugin; 8 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { resolve } from "path"; 3 | 4 | // Assume these work perfectly 5 | import { rimrafAsync, mkdirAsync } from "../src/utils/fs/async"; 6 | 7 | export const TEST_TEMP_DIR = resolve(process.cwd(), "temp"); 8 | 9 | export async function createTempTestingDir() { 10 | if (!existsSync(TEST_TEMP_DIR)) { 11 | await mkdirAsync(TEST_TEMP_DIR); 12 | } 13 | } 14 | 15 | export async function unlinkTempTestingDir() { 16 | if (existsSync(TEST_TEMP_DIR)) { 17 | await rimrafAsync(TEST_TEMP_DIR); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/util/async.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { mapAsync, filterAsync } from "../../src/utils/async"; 3 | 4 | describe("mapAsync", () => { 5 | it("Should map array ", async () => { 6 | expect(await mapAsync([1, 2, 3, 4, 5], async (num) => num * 2)).to.deep.equal([2, 4, 6, 8, 10]); 7 | }); 8 | 9 | it("Should filter array", async () => { 10 | expect(await filterAsync([1, 15, 6, 10, -1], async (num) => num >= 10)).to.deep.equal([15, 10]); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/util/download.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { existsSync, readFileSync, unlinkSync } from "fs"; 3 | import { downloadFile } from "../../src/utils/download"; 4 | 5 | describe("Download file", () => { 6 | it("Should download file to disk", async () => { 7 | const file = "download-test.json"; 8 | if (existsSync(file)) { 9 | unlinkSync(file); 10 | } 11 | expect(existsSync(file)).to.be.false; 12 | await downloadFile("https://github.com/porn-vault/porn-vault/blob/dev/tsconfig.json", file); 13 | expect(existsSync(file)).to.be.true; 14 | expect(readFileSync(file, "utf-8")).to.include("compilerOptions"); 15 | unlinkSync(file); 16 | expect(existsSync(file)).to.be.false; 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/util/hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { randomString } from "../../src/utils/hash"; 3 | 4 | describe("Hash gen", () => { 5 | it("randomString", () => { 6 | expect(randomString()).to.be.a("string").with.length(8); 7 | expect(randomString(35)).to.be.a("string").with.length(35); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/util/mergeMissingProperties.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { mergeMissingProperties } from "../../src/utils/misc"; 4 | import fixtures from "./fixtures/mergeMissingProperties.fixtures"; 5 | 6 | describe("mergeMissingProperties", () => { 7 | fixtures.forEach((fixture, fixtureIndex) => { 8 | it(`${fixtureIndex} should only add missing properties`, () => { 9 | if (fixture.noChange) { 10 | expect(fixture.target).to.deep.equal(fixture.expected); 11 | } 12 | 13 | mergeMissingProperties(fixture.target, fixture.defaults, fixture.ignorePaths || []); 14 | 15 | expect(fixture.target).to.deep.equal(fixture.expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/util/rating.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { validRating } from "../../src/utils/misc"; 3 | 4 | describe("Misc", () => { 5 | it("validRating", async () => { 6 | expect(validRating(5)).to.be.true; 7 | expect(validRating(true)).to.be.false; 8 | expect(validRating("str")).to.be.false; 9 | expect(validRating(0)).to.be.true; 10 | expect(validRating(10)).to.be.true; 11 | expect(validRating(5.6)).to.be.false; 12 | expect(validRating(-5)).to.be.false; 13 | expect(validRating(15)).to.be.false; 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/util/removeUnknownProperties.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { removeUnknownProperties } from "../../src/utils/misc"; 4 | import fixtures from "./fixtures/removeUnknownProperties.fixtures"; 5 | 6 | describe("removeUnknownProperties", () => { 7 | fixtures.forEach((fixture, fixtureIndex) => { 8 | it(`${fixtureIndex} should remove unknown properties`, () => { 9 | if (fixture.noChange) { 10 | expect(fixture.target).to.deep.equal(fixture.expected); 11 | } 12 | 13 | removeUnknownProperties(fixture.target, fixture.default, fixture.ignorePaths); 14 | 15 | expect(fixture.target).to.deep.equal(fixture.expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/util/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { isBoolean, isNumber } from "../../src/utils/types"; 3 | 4 | describe("Type utils", () => { 5 | it("isNumber", async () => { 6 | expect(isNumber(5)).to.be.true; 7 | expect(isNumber(true)).to.be.false; 8 | expect(isNumber("str")).to.be.false; 9 | }); 10 | 11 | it("isBoolean", async () => { 12 | expect(isBoolean(false)).to.be.true; 13 | expect(isBoolean("str")).to.be.false; 14 | expect(isBoolean(4)).to.be.false; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/utils/fixtures/filter_invalid_aliases.fixtures.ts: -------------------------------------------------------------------------------- 1 | export const fixtures = [ 2 | { 3 | aliases: ["Valid"], 4 | expected: ["Valid"] 5 | }, 6 | { 7 | aliases: [" "], 8 | expected: [] 9 | }, 10 | { 11 | aliases: [" "], 12 | expected: [] 13 | } 14 | ]; -------------------------------------------------------------------------------- /test/utils/fixtures/generate_timestamps.fixtures.ts: -------------------------------------------------------------------------------- 1 | export const generateTimestampsAtIntervals = [ 2 | { 3 | count: 100, 4 | duration: 100, 5 | options: { 6 | startPercentage: 0, 7 | endPercentage: 100, 8 | }, 9 | expected: new Array(100).fill(0).map((_, index) => `${(100 / 100) * index}`), 10 | }, 11 | { 12 | count: 100, 13 | duration: 100, 14 | options: { 15 | startPercentage: 2, 16 | endPercentage: 100, 17 | }, 18 | expected: new Array(100).fill(0).map((_, index) => `${2 + ((100 - 2) / 100) * index}`), 19 | }, 20 | { 21 | count: 100, 22 | duration: null, 23 | options: { 24 | startPercentage: 0, 25 | endPercentage: 100, 26 | }, 27 | expected: new Array(100).fill(0).map((_, index) => `${index.toString()}%`), 28 | }, 29 | { 30 | count: 100, 31 | duration: null, 32 | options: { 33 | startPercentage: 2, 34 | endPercentage: 100, 35 | }, 36 | expected: new Array(100).fill(0).map((_, index) => `${2 + ((100 - 2) / 100) * index}%`), 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /test/utils/fixtures/get_extension.fixture.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | ["videofile.mp4", ".mp4"], 3 | ["videofile", ""], 4 | ["./somewhere/else/video.mp4", ".mp4"], 5 | ["./somewhere/else/video", ""], 6 | ["Elena Koshka (alt. thumbnail)", ""], 7 | ]; 8 | -------------------------------------------------------------------------------- /test/utils/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 | ["Elena Koshka (alt. thumbnail)", "Elena Koshka (alt. thumbnail)"], 7 | ]; 8 | -------------------------------------------------------------------------------- /test/utils/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/utils/string.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { getExtension, removeExtension } from "../../src/utils/string"; 4 | import getExtensionFixtures from "./fixtures/get_extension.fixture"; 5 | import removeExtensionFixtures from "./fixtures/remove_extension.fixture"; 6 | 7 | describe("utils", () => { 8 | describe("String utils", () => { 9 | describe("getExtension", () => { 10 | for (const test of getExtensionFixtures) { 11 | it(`Should extract "${test[1]}" from "${test[0]}"`, () => { 12 | expect(getExtension(test[0])).equals(test[1]); 13 | }); 14 | } 15 | }); 16 | 17 | describe("removeExtension", () => { 18 | for (const test of removeExtensionFixtures) { 19 | it("Should work as expected", () => { 20 | expect(removeExtension(test[0])).equals(test[1]); 21 | }); 22 | } 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/utils/url_ext.spec.ts: -------------------------------------------------------------------------------- 1 | import { extensionFromUrl } from "../../src/utils/string"; 2 | import tests from "./fixtures/url_ext.fixture"; 3 | import { expect } from "chai"; 4 | 5 | describe("utils", () => { 6 | describe("Parse URL file extension", () => { 7 | for (const test of tests) { 8 | it(`Should get ${test[1]} from ${test[0]}`, async () => { 9 | expect(extensionFromUrl(test[0])).to.equal(test[1]); 10 | }); 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | require("fs").writeFileSync("assets/version.json", JSON.stringify({ 2 | version: require("./package.json").version 3 | })); 4 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 27 | 28 | 29 |
30 |

{{ code }}

31 |

{{{ message }}}

32 |
33 | 34 | 35 | --------------------------------------------------------------------------------