Compare commits
6 commits
5e601c8805
...
e12c3518d1
Author | SHA1 | Date | |
---|---|---|---|
e12c3518d1 | |||
a37a7d17b3 | |||
f51df8b362 | |||
c723377010 | |||
88ca3898de | |||
eb8e1debdf |
36 changed files with 2388 additions and 891 deletions
|
@ -5,4 +5,37 @@
|
||||||
max-height: 64px
|
max-height: 64px
|
||||||
|
|
||||||
.video-thumbnail
|
.video-thumbnail
|
||||||
max-height: 128px
|
width: 100%
|
||||||
|
|
||||||
|
.video-grid
|
||||||
|
$spacing: 0.5vw
|
||||||
|
|
||||||
|
display: grid
|
||||||
|
grid-row-gap: $spacing
|
||||||
|
row-gap: $spacing
|
||||||
|
grid-column-gap: $spacing
|
||||||
|
column-gap: $spacing
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr))
|
||||||
|
grid-column: auto
|
||||||
|
|
||||||
|
@include tablet
|
||||||
|
grid-template-columns: repeat(3,minmax(0,1fr))
|
||||||
|
|
||||||
|
@include desktop
|
||||||
|
grid-template-columns: repeat(4,minmax(0,1fr))
|
||||||
|
|
||||||
|
@include widescreen
|
||||||
|
grid-template-columns: repeat(5,minmax(0,1fr))
|
||||||
|
|
||||||
|
@include fullhd
|
||||||
|
grid-template-columns: repeat(6,minmax(0,1fr))
|
||||||
|
|
||||||
|
.video-card
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.video-card-content
|
||||||
|
padding: 0 0.5vw
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
padding-bottom: 0.5vw
|
||||||
|
|
|
@ -6,13 +6,6 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:6379:6379"
|
- "127.0.0.1:6379:6379"
|
||||||
|
|
||||||
rq-dashboard:
|
|
||||||
image: eoranged/rq-dashboard
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:9181:9181"
|
|
||||||
environment:
|
|
||||||
RQ_DASHBOARD_REDIS_URL: "redis://redis:6379"
|
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1
|
image: nginx:1
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
|
|
1899
package-lock.json
generated
Normal file
1899
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,6 +24,6 @@
|
||||||
"build-cleancss": "cleancss -o ucast/static/bulma/css/style.min.css ucast/static/bulma/css/style.css",
|
"build-cleancss": "cleancss -o ucast/static/bulma/css/style.min.css ucast/static/bulma/css/style.css",
|
||||||
"build-clean": "rimraf ucast/static/bulma/css",
|
"build-clean": "rimraf ucast/static/bulma/css",
|
||||||
"build-sass": "sass --style expanded --source-map assets/sass/style.sass ucast/static/bulma/css/style.css",
|
"build-sass": "sass --style expanded --source-map assets/sass/style.sass ucast/static/bulma/css/style.css",
|
||||||
"start": "npm run build-sass -- --watch"
|
"start": "sass --style expanded --watch assets/sass/style.sass _run/static/bulma/css/style.min.css"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
646
pnpm-lock.yaml
646
pnpm-lock.yaml
|
@ -1,646 +0,0 @@
|
||||||
lockfileVersion: 5.3
|
|
||||||
|
|
||||||
specifiers:
|
|
||||||
autoprefixer: ^10.4.7
|
|
||||||
bulma: ^0.9.4
|
|
||||||
clean-css-cli: ^5.6.0
|
|
||||||
postcss: ^8.4.13
|
|
||||||
postcss-cli: ^9.1.0
|
|
||||||
rimraf: ^3.0.2
|
|
||||||
sass: ^1.51.0
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
bulma: 0.9.4
|
|
||||||
|
|
||||||
devDependencies:
|
|
||||||
autoprefixer: 10.4.7_postcss@8.4.13
|
|
||||||
clean-css-cli: 5.6.0
|
|
||||||
postcss: 8.4.13
|
|
||||||
postcss-cli: 9.1.0_postcss@8.4.13
|
|
||||||
rimraf: 3.0.2
|
|
||||||
sass: 1.51.0
|
|
||||||
|
|
||||||
packages:
|
|
||||||
|
|
||||||
/@nodelib/fs.scandir/2.1.5:
|
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.stat': 2.0.5
|
|
||||||
run-parallel: 1.2.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@nodelib/fs.stat/2.0.5:
|
|
||||||
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@nodelib/fs.walk/1.2.8:
|
|
||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.scandir': 2.1.5
|
|
||||||
fastq: 1.13.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ansi-regex/5.0.1:
|
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ansi-styles/4.3.0:
|
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
color-convert: 2.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/anymatch/3.1.2:
|
|
||||||
resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
dependencies:
|
|
||||||
normalize-path: 3.0.0
|
|
||||||
picomatch: 2.3.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/array-union/3.0.1:
|
|
||||||
resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/autoprefixer/10.4.7_postcss@8.4.13:
|
|
||||||
resolution: {integrity: sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==}
|
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
postcss: ^8.1.0
|
|
||||||
dependencies:
|
|
||||||
browserslist: 4.20.3
|
|
||||||
caniuse-lite: 1.0.30001339
|
|
||||||
fraction.js: 4.2.0
|
|
||||||
normalize-range: 0.1.2
|
|
||||||
picocolors: 1.0.0
|
|
||||||
postcss: 8.4.13
|
|
||||||
postcss-value-parser: 4.2.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/balanced-match/1.0.2:
|
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/binary-extensions/2.2.0:
|
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/brace-expansion/1.1.11:
|
|
||||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
|
||||||
dependencies:
|
|
||||||
balanced-match: 1.0.2
|
|
||||||
concat-map: 0.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/braces/3.0.2:
|
|
||||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
fill-range: 7.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/browserslist/4.20.3:
|
|
||||||
resolution: {integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==}
|
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
caniuse-lite: 1.0.30001339
|
|
||||||
electron-to-chromium: 1.4.137
|
|
||||||
escalade: 3.1.1
|
|
||||||
node-releases: 2.0.4
|
|
||||||
picocolors: 1.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/bulma/0.9.4:
|
|
||||||
resolution: {integrity: sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/caniuse-lite/1.0.30001339:
|
|
||||||
resolution: {integrity: sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/chokidar/3.5.3:
|
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
|
||||||
engines: {node: '>= 8.10.0'}
|
|
||||||
dependencies:
|
|
||||||
anymatch: 3.1.2
|
|
||||||
braces: 3.0.2
|
|
||||||
glob-parent: 5.1.2
|
|
||||||
is-binary-path: 2.1.0
|
|
||||||
is-glob: 4.0.3
|
|
||||||
normalize-path: 3.0.0
|
|
||||||
readdirp: 3.6.0
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/clean-css-cli/5.6.0:
|
|
||||||
resolution: {integrity: sha512-68vorNEG808D1QzeerO9AlwQVTuaR8YSK4aqwIsjJq0wDSyPH11ApHY0O+EQrdEGUZcN+d72v+Nn/gpxjAFewQ==}
|
|
||||||
engines: {node: '>= 10.12.0'}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
chokidar: 3.5.3
|
|
||||||
clean-css: 5.3.0
|
|
||||||
commander: 7.2.0
|
|
||||||
glob: 7.2.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/clean-css/5.3.0:
|
|
||||||
resolution: {integrity: sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==}
|
|
||||||
engines: {node: '>= 10.0'}
|
|
||||||
dependencies:
|
|
||||||
source-map: 0.6.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/cliui/7.0.4:
|
|
||||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
|
||||||
dependencies:
|
|
||||||
string-width: 4.2.3
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
wrap-ansi: 7.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/color-convert/2.0.1:
|
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
|
||||||
engines: {node: '>=7.0.0'}
|
|
||||||
dependencies:
|
|
||||||
color-name: 1.1.4
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/color-name/1.1.4:
|
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/commander/7.2.0:
|
|
||||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/concat-map/0.0.1:
|
|
||||||
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/dependency-graph/0.11.0:
|
|
||||||
resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/dir-glob/3.0.1:
|
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
path-type: 4.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/electron-to-chromium/1.4.137:
|
|
||||||
resolution: {integrity: sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/emoji-regex/8.0.0:
|
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/escalade/3.1.1:
|
|
||||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fast-glob/3.2.11:
|
|
||||||
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
|
|
||||||
engines: {node: '>=8.6.0'}
|
|
||||||
dependencies:
|
|
||||||
'@nodelib/fs.stat': 2.0.5
|
|
||||||
'@nodelib/fs.walk': 1.2.8
|
|
||||||
glob-parent: 5.1.2
|
|
||||||
merge2: 1.4.1
|
|
||||||
micromatch: 4.0.5
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fastq/1.13.0:
|
|
||||||
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
|
|
||||||
dependencies:
|
|
||||||
reusify: 1.0.4
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fill-range/7.0.1:
|
|
||||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
to-regex-range: 5.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fraction.js/4.2.0:
|
|
||||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fs-extra/10.1.0:
|
|
||||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
graceful-fs: 4.2.10
|
|
||||||
jsonfile: 6.1.0
|
|
||||||
universalify: 2.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fs.realpath/1.0.0:
|
|
||||||
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fsevents/2.3.2:
|
|
||||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/get-caller-file/2.0.5:
|
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/get-stdin/9.0.0:
|
|
||||||
resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/glob-parent/5.1.2:
|
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
dependencies:
|
|
||||||
is-glob: 4.0.3
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/glob/7.2.0:
|
|
||||||
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
|
||||||
dependencies:
|
|
||||||
fs.realpath: 1.0.0
|
|
||||||
inflight: 1.0.6
|
|
||||||
inherits: 2.0.4
|
|
||||||
minimatch: 3.1.2
|
|
||||||
once: 1.4.0
|
|
||||||
path-is-absolute: 1.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/globby/12.2.0:
|
|
||||||
resolution: {integrity: sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==}
|
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
|
||||||
dependencies:
|
|
||||||
array-union: 3.0.1
|
|
||||||
dir-glob: 3.0.1
|
|
||||||
fast-glob: 3.2.11
|
|
||||||
ignore: 5.2.0
|
|
||||||
merge2: 1.4.1
|
|
||||||
slash: 4.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/graceful-fs/4.2.10:
|
|
||||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ignore/5.2.0:
|
|
||||||
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
|
|
||||||
engines: {node: '>= 4'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/immutable/4.0.0:
|
|
||||||
resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/inflight/1.0.6:
|
|
||||||
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
|
|
||||||
dependencies:
|
|
||||||
once: 1.4.0
|
|
||||||
wrappy: 1.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/inherits/2.0.4:
|
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-binary-path/2.1.0:
|
|
||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
binary-extensions: 2.2.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-extglob/2.1.1:
|
|
||||||
resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-fullwidth-code-point/3.0.0:
|
|
||||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-glob/4.0.3:
|
|
||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dependencies:
|
|
||||||
is-extglob: 2.1.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-number/7.0.0:
|
|
||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
|
||||||
engines: {node: '>=0.12.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/jsonfile/6.1.0:
|
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
|
||||||
dependencies:
|
|
||||||
universalify: 2.0.0
|
|
||||||
optionalDependencies:
|
|
||||||
graceful-fs: 4.2.10
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lilconfig/2.0.5:
|
|
||||||
resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/merge2/1.4.1:
|
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/micromatch/4.0.5:
|
|
||||||
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
|
|
||||||
engines: {node: '>=8.6'}
|
|
||||||
dependencies:
|
|
||||||
braces: 3.0.2
|
|
||||||
picomatch: 2.3.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/minimatch/3.1.2:
|
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
|
||||||
dependencies:
|
|
||||||
brace-expansion: 1.1.11
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/nanoid/3.3.4:
|
|
||||||
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
|
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
|
||||||
hasBin: true
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/node-releases/2.0.4:
|
|
||||||
resolution: {integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/normalize-path/3.0.0:
|
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/normalize-range/0.1.2:
|
|
||||||
resolution: {integrity: sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/once/1.4.0:
|
|
||||||
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
|
||||||
dependencies:
|
|
||||||
wrappy: 1.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-is-absolute/1.0.1:
|
|
||||||
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-type/4.0.0:
|
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/picocolors/1.0.0:
|
|
||||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/picomatch/2.3.1:
|
|
||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
|
||||||
engines: {node: '>=8.6'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pify/2.3.0:
|
|
||||||
resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/postcss-cli/9.1.0_postcss@8.4.13:
|
|
||||||
resolution: {integrity: sha512-zvDN2ADbWfza42sAnj+O2uUWyL0eRL1V+6giM2vi4SqTR3gTYy8XzcpfwccayF2szcUif0HMmXiEaDv9iEhcpw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
postcss: ^8.0.0
|
|
||||||
dependencies:
|
|
||||||
chokidar: 3.5.3
|
|
||||||
dependency-graph: 0.11.0
|
|
||||||
fs-extra: 10.1.0
|
|
||||||
get-stdin: 9.0.0
|
|
||||||
globby: 12.2.0
|
|
||||||
picocolors: 1.0.0
|
|
||||||
postcss: 8.4.13
|
|
||||||
postcss-load-config: 3.1.4_postcss@8.4.13
|
|
||||||
postcss-reporter: 7.0.5_postcss@8.4.13
|
|
||||||
pretty-hrtime: 1.0.3
|
|
||||||
read-cache: 1.0.0
|
|
||||||
slash: 4.0.0
|
|
||||||
yargs: 17.4.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- ts-node
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/postcss-load-config/3.1.4_postcss@8.4.13:
|
|
||||||
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
peerDependencies:
|
|
||||||
postcss: '>=8.0.9'
|
|
||||||
ts-node: '>=9.0.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
postcss:
|
|
||||||
optional: true
|
|
||||||
ts-node:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
lilconfig: 2.0.5
|
|
||||||
postcss: 8.4.13
|
|
||||||
yaml: 1.10.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/postcss-reporter/7.0.5_postcss@8.4.13:
|
|
||||||
resolution: {integrity: sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
postcss: ^8.1.0
|
|
||||||
dependencies:
|
|
||||||
picocolors: 1.0.0
|
|
||||||
postcss: 8.4.13
|
|
||||||
thenby: 1.3.4
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/postcss-value-parser/4.2.0:
|
|
||||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/postcss/8.4.13:
|
|
||||||
resolution: {integrity: sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==}
|
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
|
||||||
dependencies:
|
|
||||||
nanoid: 3.3.4
|
|
||||||
picocolors: 1.0.0
|
|
||||||
source-map-js: 1.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pretty-hrtime/1.0.3:
|
|
||||||
resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/queue-microtask/1.2.3:
|
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/read-cache/1.0.0:
|
|
||||||
resolution: {integrity: sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=}
|
|
||||||
dependencies:
|
|
||||||
pify: 2.3.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/readdirp/3.6.0:
|
|
||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
|
||||||
engines: {node: '>=8.10.0'}
|
|
||||||
dependencies:
|
|
||||||
picomatch: 2.3.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/require-directory/2.1.1:
|
|
||||||
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/reusify/1.0.4:
|
|
||||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/rimraf/3.0.2:
|
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
glob: 7.2.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/run-parallel/1.2.0:
|
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
|
||||||
dependencies:
|
|
||||||
queue-microtask: 1.2.3
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/sass/1.51.0:
|
|
||||||
resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
dependencies:
|
|
||||||
chokidar: 3.5.3
|
|
||||||
immutable: 4.0.0
|
|
||||||
source-map-js: 1.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/slash/4.0.0:
|
|
||||||
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/source-map-js/1.0.2:
|
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/source-map/0.6.1:
|
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/string-width/4.2.3:
|
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
emoji-regex: 8.0.0
|
|
||||||
is-fullwidth-code-point: 3.0.0
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/strip-ansi/6.0.1:
|
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
dependencies:
|
|
||||||
ansi-regex: 5.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/thenby/1.3.4:
|
|
||||||
resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/to-regex-range/5.0.1:
|
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
|
||||||
engines: {node: '>=8.0'}
|
|
||||||
dependencies:
|
|
||||||
is-number: 7.0.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/universalify/2.0.0:
|
|
||||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
|
||||||
engines: {node: '>= 10.0.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/wrap-ansi/7.0.0:
|
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dependencies:
|
|
||||||
ansi-styles: 4.3.0
|
|
||||||
string-width: 4.2.3
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/wrappy/1.0.2:
|
|
||||||
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/y18n/5.0.8:
|
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yaml/1.10.2:
|
|
||||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yargs-parser/21.0.1:
|
|
||||||
resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yargs/17.4.1:
|
|
||||||
resolution: {integrity: sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
cliui: 7.0.4
|
|
||||||
escalade: 3.1.1
|
|
||||||
get-caller-file: 2.0.5
|
|
||||||
require-directory: 2.1.1
|
|
||||||
string-width: 4.2.3
|
|
||||||
y18n: 5.0.8
|
|
||||||
yargs-parser: 21.0.1
|
|
||||||
dev: true
|
|
103
poetry.lock
generated
103
poetry.lock
generated
|
@ -128,14 +128,14 @@ Pillow = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "6.4"
|
version = "6.4.1"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
tomli = {version = "*", optional = true, markers = "python_version < \"3.11\" and extra == \"toml\""}
|
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
toml = ["tomli"]
|
||||||
|
@ -201,6 +201,17 @@ python-versions = ">=3.7"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
django = ">=2.2"
|
django = ">=2.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-htmx"
|
||||||
|
version = "1.12.0"
|
||||||
|
description = "Extensions for using Django with htmx."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fakeredis"
|
name = "fakeredis"
|
||||||
version = "1.8"
|
version = "1.8"
|
||||||
|
@ -743,7 +754,7 @@ websockets = "*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "1d1799636eadf391bd9545eb3d8750a1e882cdb9b84d47395284d90a3cfeb609"
|
content-hash = "5c24c22351390a472cd905bf1d08890314441ef590c46a64e7940fb180f909a2"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -935,47 +946,47 @@ colorthief = [
|
||||||
{file = "colorthief-0.2.1.tar.gz", hash = "sha256:079cb0c95bdd669c4643e2f7494de13b0b6029d5cdbe2d74d5d3c3386bd57221"},
|
{file = "colorthief-0.2.1.tar.gz", hash = "sha256:079cb0c95bdd669c4643e2f7494de13b0b6029d5cdbe2d74d5d3c3386bd57221"},
|
||||||
]
|
]
|
||||||
coverage = [
|
coverage = [
|
||||||
{file = "coverage-6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf"},
|
{file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"},
|
||||||
{file = "coverage-6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f"},
|
{file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"},
|
||||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41"},
|
{file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"},
|
||||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088"},
|
{file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"},
|
||||||
{file = "coverage-6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701"},
|
{file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"},
|
||||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea"},
|
{file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"},
|
||||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39"},
|
{file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"},
|
||||||
{file = "coverage-6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632"},
|
{file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"},
|
||||||
{file = "coverage-6.4-cp310-cp310-win32.whl", hash = "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f"},
|
{file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"},
|
||||||
{file = "coverage-6.4-cp310-cp310-win_amd64.whl", hash = "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720"},
|
{file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383"},
|
{file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08"},
|
{file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6"},
|
{file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d"},
|
{file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7"},
|
{file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052"},
|
{file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211"},
|
{file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-win32.whl", hash = "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a"},
|
{file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"},
|
||||||
{file = "coverage-6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311"},
|
{file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"},
|
||||||
{file = "coverage-6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61"},
|
{file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"},
|
||||||
{file = "coverage-6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f"},
|
{file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"},
|
||||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce"},
|
{file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"},
|
||||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c"},
|
{file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"},
|
||||||
{file = "coverage-6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95"},
|
{file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"},
|
||||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c"},
|
{file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"},
|
||||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6"},
|
{file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"},
|
||||||
{file = "coverage-6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166"},
|
{file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"},
|
||||||
{file = "coverage-6.4-cp38-cp38-win32.whl", hash = "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426"},
|
{file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"},
|
||||||
{file = "coverage-6.4-cp38-cp38-win_amd64.whl", hash = "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3"},
|
{file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"},
|
||||||
{file = "coverage-6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740"},
|
{file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"},
|
||||||
{file = "coverage-6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5"},
|
{file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"},
|
||||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a"},
|
{file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"},
|
||||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65"},
|
{file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"},
|
||||||
{file = "coverage-6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c"},
|
{file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"},
|
||||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df"},
|
{file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"},
|
||||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018"},
|
{file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"},
|
||||||
{file = "coverage-6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f"},
|
{file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"},
|
||||||
{file = "coverage-6.4-cp39-cp39-win32.whl", hash = "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3"},
|
{file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"},
|
||||||
{file = "coverage-6.4-cp39-cp39-win_amd64.whl", hash = "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055"},
|
{file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"},
|
||||||
{file = "coverage-6.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45"},
|
{file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"},
|
||||||
{file = "coverage-6.4.tar.gz", hash = "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49"},
|
{file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"},
|
||||||
]
|
]
|
||||||
croniter = [
|
croniter = [
|
||||||
{file = "croniter-1.3.5-py2.py3-none-any.whl", hash = "sha256:4f72faca42c00beb6e30907f1315145f43dfbe5ec0ad4ada24b4c0d57b86a33a"},
|
{file = "croniter-1.3.5-py2.py3-none-any.whl", hash = "sha256:4f72faca42c00beb6e30907f1315145f43dfbe5ec0ad4ada24b4c0d57b86a33a"},
|
||||||
|
@ -997,6 +1008,10 @@ django-bulma = [
|
||||||
{file = "django-bulma-0.8.3.tar.gz", hash = "sha256:b794b4e64f482de77f376451f7cd8b3c8448eb68e5a24c51b9190625a08b0b30"},
|
{file = "django-bulma-0.8.3.tar.gz", hash = "sha256:b794b4e64f482de77f376451f7cd8b3c8448eb68e5a24c51b9190625a08b0b30"},
|
||||||
{file = "django_bulma-0.8.3-py3-none-any.whl", hash = "sha256:0ef6e5c171c2a32010e724a8be61ba6cd0e55ebbd242cf6780560518483c4d00"},
|
{file = "django_bulma-0.8.3-py3-none-any.whl", hash = "sha256:0ef6e5c171c2a32010e724a8be61ba6cd0e55ebbd242cf6780560518483c4d00"},
|
||||||
]
|
]
|
||||||
|
django-htmx = [
|
||||||
|
{file = "django-htmx-1.12.0.tar.gz", hash = "sha256:33750914939e8fbd214e36fb0976003e06f4cadc92007cbe5c49f59e32e58de6"},
|
||||||
|
{file = "django_htmx-1.12.0-py3-none-any.whl", hash = "sha256:e8351b9251642a5a550a18c6958727ea9b33574bb412b1900fa5ab0d5dd9db40"},
|
||||||
|
]
|
||||||
fakeredis = [
|
fakeredis = [
|
||||||
{file = "fakeredis-1.8-py3-none-any.whl", hash = "sha256:65dcd78c0cd29d17daccce9f58698f6ab61ad7a404eab373fcad2b76fe8db03d"},
|
{file = "fakeredis-1.8-py3-none-any.whl", hash = "sha256:65dcd78c0cd29d17daccce9f58698f6ab61ad7a404eab373fcad2b76fe8db03d"},
|
||||||
{file = "fakeredis-1.8.tar.gz", hash = "sha256:cbf8d74ae06672d40b2fa88b9ee4f1d6efd56b06b2e7f0be2c639647f00643f1"},
|
{file = "fakeredis-1.8.tar.gz", hash = "sha256:cbf8d74ae06672d40b2fa88b9ee4f1d6efd56b06b2e7f0be2c639647f00643f1"},
|
||||||
|
|
|
@ -28,6 +28,7 @@ mutagen = "^1.45.1"
|
||||||
rq = "^1.10.1"
|
rq = "^1.10.1"
|
||||||
rq-scheduler = "^0.11.0"
|
rq-scheduler = "^0.11.0"
|
||||||
pycryptodomex = "^3.14.1"
|
pycryptodomex = "^3.14.1"
|
||||||
|
django-htmx = "^1.12.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.1.1"
|
pytest = "^7.1.1"
|
||||||
|
|
2
tasks.py
2
tasks.py
|
@ -115,8 +115,10 @@ def reset(c):
|
||||||
shutil.rmtree(DIR_DOWNLOAD)
|
shutil.rmtree(DIR_DOWNLOAD)
|
||||||
if FILE_DB.exists():
|
if FILE_DB.exists():
|
||||||
os.remove(FILE_DB)
|
os.remove(FILE_DB)
|
||||||
|
os.makedirs(DIR_DOWNLOAD, exist_ok=True)
|
||||||
migrate(c)
|
migrate(c)
|
||||||
create_testuser(c)
|
create_testuser(c)
|
||||||
|
collectstatic(c)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
|
|
|
@ -2,6 +2,7 @@ import re
|
||||||
from xml.sax import saxutils
|
from xml.sax import saxutils
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.contrib.syndication.views import Feed, add_domain
|
from django.contrib.syndication.views import Feed, add_domain
|
||||||
from django.utils import feedgenerator
|
from django.utils import feedgenerator
|
||||||
|
@ -155,7 +156,9 @@ class UcastFeed(Feed):
|
||||||
image_url=self.full_link_url(request, f"/files/avatar/{channel.slug}.jpg"),
|
image_url=self.full_link_url(request, f"/files/avatar/{channel.slug}.jpg"),
|
||||||
)
|
)
|
||||||
|
|
||||||
for video in channel.video_set.order_by("-published"):
|
for video in channel.video_set.order_by("-published")[
|
||||||
|
: settings.FEED_MAX_ITEMS
|
||||||
|
]:
|
||||||
feed.add_item(
|
feed.add_item(
|
||||||
title=video.title,
|
title=video.title,
|
||||||
link=video.get_absolute_url(),
|
link=video.get_absolute_url(),
|
||||||
|
@ -187,6 +190,6 @@ class UcastFeed(Feed):
|
||||||
request, f"/files/audio/{item.channel.slug}/{item.slug}.mp3"
|
request, f"/files/audio/{item.channel.slug}/{item.slug}.mp3"
|
||||||
),
|
),
|
||||||
length=str(item.download_size),
|
length=str(item.download_size),
|
||||||
mime_type="audio/mpg",
|
mime_type="audio/mpeg",
|
||||||
)
|
)
|
||||||
return [enc]
|
return [enc]
|
||||||
|
|
|
@ -3,3 +3,7 @@ from django import forms
|
||||||
|
|
||||||
class AddChannelForm(forms.Form):
|
class AddChannelForm(forms.Form):
|
||||||
channel_str = forms.CharField(label="Channel-ID / URL")
|
channel_str = forms.CharField(label="Channel-ID / URL")
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteVideoForm(forms.Form):
|
||||||
|
id = forms.IntegerField()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.0.4 on 2022-05-29 21:01
|
# Generated by Django 4.0.4 on 2022-06-21 23:07
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
|
@ -28,15 +28,19 @@ class Migration(migrations.Migration):
|
||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("channel_id", models.CharField(max_length=30)),
|
("channel_id", models.CharField(db_index=True, max_length=30)),
|
||||||
("name", models.CharField(max_length=100)),
|
("name", models.CharField(max_length=100)),
|
||||||
("slug", models.CharField(max_length=100)),
|
("slug", models.CharField(db_index=True, max_length=100)),
|
||||||
("description", models.TextField()),
|
("description", models.TextField()),
|
||||||
("subscribers", models.CharField(max_length=20, null=True)),
|
("subscribers", models.CharField(max_length=20, null=True)),
|
||||||
("active", models.BooleanField(default=True)),
|
("active", models.BooleanField(default=True)),
|
||||||
("skip_livestreams", models.BooleanField(default=True)),
|
("skip_livestreams", models.BooleanField(default=True)),
|
||||||
("skip_shorts", models.BooleanField(default=True)),
|
("skip_shorts", models.BooleanField(default=True)),
|
||||||
("avatar_url", models.CharField(max_length=250, null=True)),
|
("avatar_url", models.CharField(max_length=250, null=True)),
|
||||||
|
(
|
||||||
|
"last_update",
|
||||||
|
models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -51,9 +55,9 @@ class Migration(migrations.Migration):
|
||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("video_id", models.CharField(max_length=30)),
|
("video_id", models.CharField(db_index=True, max_length=30)),
|
||||||
("title", models.CharField(max_length=200)),
|
("title", models.CharField(max_length=200)),
|
||||||
("slug", models.CharField(max_length=209)),
|
("slug", models.CharField(db_index=True, max_length=209)),
|
||||||
("published", models.DateTimeField()),
|
("published", models.DateTimeField()),
|
||||||
("downloaded", models.DateTimeField(null=True)),
|
("downloaded", models.DateTimeField(null=True)),
|
||||||
("description", models.TextField()),
|
("description", models.TextField()),
|
||||||
|
@ -61,6 +65,7 @@ class Migration(migrations.Migration):
|
||||||
("is_livestream", models.BooleanField(default=False)),
|
("is_livestream", models.BooleanField(default=False)),
|
||||||
("is_short", models.BooleanField(default=False)),
|
("is_short", models.BooleanField(default=False)),
|
||||||
("download_size", models.IntegerField(null=True)),
|
("download_size", models.IntegerField(null=True)),
|
||||||
|
("is_deleted", models.BooleanField(default=False)),
|
||||||
(
|
(
|
||||||
"channel",
|
"channel",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
||||||
from Cryptodome import Random
|
from Cryptodome import Random
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from ucast.service import util
|
from ucast.service import util
|
||||||
|
|
||||||
|
@ -31,15 +32,16 @@ def _get_unique_slug(
|
||||||
|
|
||||||
|
|
||||||
class Channel(models.Model):
|
class Channel(models.Model):
|
||||||
channel_id = models.CharField(max_length=30)
|
channel_id = models.CharField(max_length=30, db_index=True)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
slug = models.CharField(max_length=100)
|
slug = models.CharField(max_length=100, db_index=True)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
subscribers = models.CharField(max_length=20, null=True)
|
subscribers = models.CharField(max_length=20, null=True)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
skip_livestreams = models.BooleanField(default=True)
|
skip_livestreams = models.BooleanField(default=True)
|
||||||
skip_shorts = models.BooleanField(default=True)
|
skip_shorts = models.BooleanField(default=True)
|
||||||
avatar_url = models.CharField(max_length=250, null=True)
|
avatar_url = models.CharField(max_length=250, null=True)
|
||||||
|
last_update = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_new_slug(cls, name: str) -> str:
|
def get_new_slug(cls, name: str) -> str:
|
||||||
|
@ -73,9 +75,9 @@ class Channel(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Video(models.Model):
|
class Video(models.Model):
|
||||||
video_id = models.CharField(max_length=30)
|
video_id = models.CharField(max_length=30, db_index=True)
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
slug = models.CharField(max_length=209)
|
slug = models.CharField(max_length=209, db_index=True)
|
||||||
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
||||||
published = models.DateTimeField()
|
published = models.DateTimeField()
|
||||||
downloaded = models.DateTimeField(null=True)
|
downloaded = models.DateTimeField(null=True)
|
||||||
|
@ -84,6 +86,7 @@ class Video(models.Model):
|
||||||
is_livestream = models.BooleanField(default=False)
|
is_livestream = models.BooleanField(default=False)
|
||||||
is_short = models.BooleanField(default=False)
|
is_short = models.BooleanField(default=False)
|
||||||
download_size = models.IntegerField(null=True)
|
download_size = models.IntegerField(null=True)
|
||||||
|
is_deleted = models.BooleanField(default=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_new_slug(cls, title: str, date: datetime.date, channel_id: str) -> str:
|
def get_new_slug(cls, title: str, date: datetime.date, channel_id: str) -> str:
|
||||||
|
|
72
ucast/service/controller.py
Normal file
72
ucast/service/controller.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from ucast.models import Channel, Video
|
||||||
|
from ucast.service import storage, util, youtube
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelAlreadyExistsException(Exception):
|
||||||
|
def __init__(self, *args: object) -> None:
|
||||||
|
super().__init__("channel already exists", *args)
|
||||||
|
|
||||||
|
|
||||||
|
def download_channel_avatar(channel: Channel):
|
||||||
|
store = storage.Storage()
|
||||||
|
channel_folder = store.get_or_create_channel_folder(channel.slug)
|
||||||
|
util.download_image_file(channel.avatar_url, channel_folder.file_avatar)
|
||||||
|
util.resize_avatar(channel_folder.file_avatar, channel_folder.file_avatar_sm)
|
||||||
|
|
||||||
|
|
||||||
|
def create_channel(channel_str: str) -> Channel:
|
||||||
|
if youtube.CHANID_REGEX.match(channel_str):
|
||||||
|
if Channel.objects.filter(channel_id=channel_str).exists():
|
||||||
|
raise ChannelAlreadyExistsException()
|
||||||
|
|
||||||
|
channel_url = youtube.channel_url_from_str(channel_str)
|
||||||
|
channel_data = youtube.get_channel_metadata(channel_url)
|
||||||
|
|
||||||
|
if Channel.objects.filter(channel_id=channel_data.id).exists():
|
||||||
|
raise ChannelAlreadyExistsException()
|
||||||
|
|
||||||
|
channel_slug = Channel.get_new_slug(channel_data.name)
|
||||||
|
|
||||||
|
channel = Channel(
|
||||||
|
channel_id=channel_data.id,
|
||||||
|
name=channel_data.name,
|
||||||
|
slug=channel_slug,
|
||||||
|
description=channel_data.description,
|
||||||
|
subscribers=channel_data.subscribers,
|
||||||
|
avatar_url=channel_data.avatar_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
download_channel_avatar(channel)
|
||||||
|
|
||||||
|
channel.save()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
def delete_video(id: int):
|
||||||
|
video = Video.objects.get(id=id)
|
||||||
|
|
||||||
|
store = storage.Storage()
|
||||||
|
channel_folder = store.get_channel_folder(video.channel.slug)
|
||||||
|
|
||||||
|
util.remove_if_exists(channel_folder.get_audio(video.slug))
|
||||||
|
util.remove_if_exists(channel_folder.get_cover(video.slug))
|
||||||
|
util.remove_if_exists(channel_folder.get_thumbnail(video.slug))
|
||||||
|
util.remove_if_exists(channel_folder.get_thumbnail(video.slug, True))
|
||||||
|
|
||||||
|
video.is_deleted = True
|
||||||
|
video.downloaded = None
|
||||||
|
video.download_size = None
|
||||||
|
video.save()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_channel(id: int):
|
||||||
|
channel = Channel.objects.get(id=id)
|
||||||
|
|
||||||
|
store = storage.Storage()
|
||||||
|
channel_folder = store.get_channel_folder(channel.slug)
|
||||||
|
|
||||||
|
shutil.rmtree(channel_folder.dir_root)
|
||||||
|
|
||||||
|
channel.delete()
|
|
@ -9,7 +9,7 @@ from colorthief import ColorThief
|
||||||
from fonts.ttf import SourceSansPro
|
from fonts.ttf import SourceSansPro
|
||||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
||||||
|
|
||||||
from ucast.service import typ
|
from ucast.service import typ, util
|
||||||
|
|
||||||
COVER_STYLE_BLUR = "blur"
|
COVER_STYLE_BLUR = "blur"
|
||||||
COVER_STYLE_GRADIENT = "gradient"
|
COVER_STYLE_GRADIENT = "gradient"
|
||||||
|
@ -105,8 +105,9 @@ def _draw_text_box(
|
||||||
x_tl, y_tl, x_br, y_br = box
|
x_tl, y_tl, x_br, y_br = box
|
||||||
height = y_br - y_tl
|
height = y_br - y_tl
|
||||||
width = x_br - x_tl
|
width = x_br - x_tl
|
||||||
|
sanitized_text = util.strip_emoji(text)
|
||||||
|
|
||||||
lines = _split_text(height, width, text, font, line_spacing)
|
lines = _split_text(height, width, sanitized_text, font, line_spacing)
|
||||||
|
|
||||||
y_start = y_tl
|
y_start = y_tl
|
||||||
if vertical_center:
|
if vertical_center:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
@ -13,6 +15,23 @@ from PIL import Image
|
||||||
AVATAR_SM_WIDTH = 100
|
AVATAR_SM_WIDTH = 100
|
||||||
THUMBNAIL_SM_WIDTH = 360
|
THUMBNAIL_SM_WIDTH = 360
|
||||||
|
|
||||||
|
EMOJI_PATTERN = re.compile(
|
||||||
|
"["
|
||||||
|
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||||
|
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
||||||
|
"\U0001F600-\U0001F64F" # emoticons
|
||||||
|
"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||||
|
"\U0001F700-\U0001F77F" # alchemical symbols
|
||||||
|
"\U0001F780-\U0001F7FF" # Geometric Shapes Extended
|
||||||
|
"\U0001F800-\U0001F8FF" # Supplemental Arrows-C
|
||||||
|
"\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
|
||||||
|
"\U0001FA00-\U0001FA6F" # Chess Symbols
|
||||||
|
"\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A
|
||||||
|
"\U00002702-\U000027B0" # Dingbats
|
||||||
|
"\U000024C2-\U0001F251"
|
||||||
|
"]+"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_file(url: str, download_path: Path):
|
def download_file(url: str, download_path: Path):
|
||||||
r = requests.get(url, allow_redirects=True)
|
r = requests.get(url, allow_redirects=True)
|
||||||
|
@ -163,3 +182,13 @@ def add_key_to_url(url: str, key: str):
|
||||||
query["key"] = key
|
query["key"] = key
|
||||||
url_parts[4] = _urlencode(query)
|
url_parts[4] = _urlencode(query)
|
||||||
return parse.urlunparse(url_parts)
|
return parse.urlunparse(url_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_if_exists(file: Path):
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_emoji(str_in: str) -> str:
|
||||||
|
stripped = EMOJI_PATTERN.sub("", str_in)
|
||||||
|
return re.sub(" +", " ", stripped)
|
||||||
|
|
|
@ -133,6 +133,14 @@ def get_video_details(video_id: str) -> VideoDetails:
|
||||||
def download_audio(
|
def download_audio(
|
||||||
video_id: str, download_path: Path, sponsorblock=False
|
video_id: str, download_path: Path, sponsorblock=False
|
||||||
) -> VideoDetails:
|
) -> VideoDetails:
|
||||||
|
"""
|
||||||
|
Download the audio track from a YouTube video save it at the given filepath.
|
||||||
|
|
||||||
|
:param video_id: YouTube video ID
|
||||||
|
:param download_path: Download path
|
||||||
|
:param sponsorblock: Enable Sponsorblock
|
||||||
|
:return: VideoDetails
|
||||||
|
"""
|
||||||
tmp_dld_file = download_path.with_suffix(".dld" + download_path.suffix)
|
tmp_dld_file = download_path.with_suffix(".dld" + download_path.suffix)
|
||||||
|
|
||||||
ydl_params = {
|
ydl_params = {
|
||||||
|
@ -160,7 +168,8 @@ def download_audio(
|
||||||
# extract_info downloads the video and returns its metadata
|
# extract_info downloads the video and returns its metadata
|
||||||
info = ydl.extract_info(video_id)
|
info = ydl.extract_info(video_id)
|
||||||
|
|
||||||
shutil.move(tmp_dld_file, download_path)
|
downloaded_file = info["requested_downloads"][0]["filepath"]
|
||||||
|
shutil.move(downloaded_file, download_path)
|
||||||
return VideoDetails.from_vinfo(info)
|
return VideoDetails.from_vinfo(info)
|
||||||
|
|
||||||
|
|
||||||
|
|
6
ucast/static/ucast/css/fontawesome.css
vendored
Normal file
6
ucast/static/ucast/css/fontawesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
ucast/static/ucast/js/htmx.min.js
vendored
Normal file
2
ucast/static/ucast/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
ucast/static/ucast/js/ucast.js
Normal file
9
ucast/static/ucast/js/ucast.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const confirmButtons = document.getElementsByClassName("dialog-confirm")
|
||||||
|
for(let btn of confirmButtons) {
|
||||||
|
btn.addEventListener("click", function(e) {
|
||||||
|
const result = window.confirm(btn.getAttribute("confirm-msg"));
|
||||||
|
if (!result) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
BIN
ucast/static/ucast/webfonts/fa-brands-400.ttf
Normal file
BIN
ucast/static/ucast/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-brands-400.woff2
Normal file
BIN
ucast/static/ucast/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-regular-400.ttf
Normal file
BIN
ucast/static/ucast/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-regular-400.woff2
Normal file
BIN
ucast/static/ucast/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-solid-900.ttf
Normal file
BIN
ucast/static/ucast/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-solid-900.woff2
Normal file
BIN
ucast/static/ucast/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-v4compatibility.ttf
Normal file
BIN
ucast/static/ucast/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
ucast/static/ucast/webfonts/fa-v4compatibility.woff2
Normal file
BIN
ucast/static/ucast/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
|
@ -1,73 +1,47 @@
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.db.models import ObjectDoesNotExist
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from ucast import queue
|
from ucast import queue
|
||||||
from ucast.models import Channel, Video
|
from ucast.models import Channel, Video
|
||||||
from ucast.service import cover, storage, util, videoutil, youtube
|
from ucast.service import controller, cover, storage, util, videoutil, youtube
|
||||||
|
|
||||||
|
|
||||||
def _get_or_create_channel(channel_str: str) -> Channel:
|
|
||||||
if youtube.CHANID_REGEX.match(channel_str):
|
|
||||||
try:
|
|
||||||
return Channel.objects.get(channel_id=channel_str)
|
|
||||||
except Channel.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
channel_url = youtube.channel_url_from_str(channel_str)
|
|
||||||
channel_data = youtube.get_channel_metadata(channel_url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return Channel.objects.get(channel_id=channel_data.id)
|
|
||||||
except Channel.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
channel_slug = Channel.get_new_slug(channel_data.name)
|
|
||||||
store = storage.Storage()
|
|
||||||
channel_folder = store.get_or_create_channel_folder(channel_slug)
|
|
||||||
|
|
||||||
util.download_image_file(channel_data.avatar_url, channel_folder.file_avatar)
|
|
||||||
util.resize_avatar(channel_folder.file_avatar, channel_folder.file_avatar_sm)
|
|
||||||
|
|
||||||
channel = Channel(
|
|
||||||
channel_id=channel_data.id,
|
|
||||||
name=channel_data.name,
|
|
||||||
slug=channel_slug,
|
|
||||||
description=channel_data.description,
|
|
||||||
subscribers=channel_data.subscribers,
|
|
||||||
)
|
|
||||||
channel.save()
|
|
||||||
return channel
|
|
||||||
|
|
||||||
|
|
||||||
def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
def _load_scraped_video(vid: youtube.VideoScraped, channel: Channel):
|
||||||
if Video.objects.filter(video_id=vid.id).exists():
|
# Create video object if it does not exist
|
||||||
return
|
try:
|
||||||
|
video = Video.objects.get(video_id=vid.id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
details = youtube.get_video_details(vid.id)
|
||||||
|
|
||||||
details = youtube.get_video_details(vid.id)
|
# Dont load active livestreams
|
||||||
|
if details.is_currently_live:
|
||||||
|
return
|
||||||
|
|
||||||
# Dont load active livestreams
|
slug = Video.get_new_slug(
|
||||||
if details.is_currently_live:
|
details.title, details.published.date(), channel.channel_id
|
||||||
return
|
)
|
||||||
|
|
||||||
slug = Video.get_new_slug(
|
video = Video(
|
||||||
details.title, details.published.date(), channel.channel_id
|
video_id=details.id,
|
||||||
)
|
title=details.title,
|
||||||
|
slug=slug,
|
||||||
|
channel=channel,
|
||||||
|
published=details.published,
|
||||||
|
description=details.description,
|
||||||
|
duration=details.duration,
|
||||||
|
is_livestream=details.is_livestream,
|
||||||
|
is_short=details.is_short,
|
||||||
|
)
|
||||||
|
video.save()
|
||||||
|
|
||||||
video = Video(
|
if (
|
||||||
video_id=details.id,
|
video.downloaded is None
|
||||||
title=details.title,
|
and video.is_deleted is False
|
||||||
slug=slug,
|
and channel.should_download(video)
|
||||||
channel=channel,
|
):
|
||||||
published=details.published,
|
|
||||||
description=details.description,
|
|
||||||
duration=details.duration,
|
|
||||||
is_livestream=details.is_livestream,
|
|
||||||
is_short=details.is_short,
|
|
||||||
)
|
|
||||||
video.save()
|
|
||||||
|
|
||||||
if channel.should_download(video):
|
|
||||||
queue.enqueue(download_video, video)
|
queue.enqueue(download_video, video)
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,6 +63,10 @@ def download_video(video: Video):
|
||||||
youtube.download_thumbnail(details, tn_path)
|
youtube.download_thumbnail(details, tn_path)
|
||||||
util.resize_thumbnail(tn_path, channel_folder.get_thumbnail(video.slug, True))
|
util.resize_thumbnail(tn_path, channel_folder.get_thumbnail(video.slug, True))
|
||||||
cover_file = channel_folder.get_cover(video.slug)
|
cover_file = channel_folder.get_cover(video.slug)
|
||||||
|
|
||||||
|
if not os.path.isfile(channel_folder.file_avatar):
|
||||||
|
controller.download_channel_avatar(video.channel)
|
||||||
|
|
||||||
cover.create_cover_file(
|
cover.create_cover_file(
|
||||||
tn_path,
|
tn_path,
|
||||||
channel_folder.file_avatar,
|
channel_folder.file_avatar,
|
||||||
|
@ -109,25 +87,10 @@ def download_video(video: Video):
|
||||||
|
|
||||||
video.downloaded = timezone.now()
|
video.downloaded = timezone.now()
|
||||||
video.download_size = os.path.getsize(audio_file)
|
video.download_size = os.path.getsize(audio_file)
|
||||||
|
video.download_type, _ = mimetypes.guess_type(audio_file)
|
||||||
video.save()
|
video.save()
|
||||||
|
|
||||||
|
|
||||||
def import_channel(channel_str: str, limit: int = None):
|
|
||||||
"""
|
|
||||||
Add a new channel to ucast and download all existing videos.
|
|
||||||
|
|
||||||
:param channel_str: YT-Channel-ID / URL
|
|
||||||
:param limit: Maximum number of videos to download
|
|
||||||
"""
|
|
||||||
channel = _get_or_create_channel(channel_str)
|
|
||||||
|
|
||||||
if limit == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
for vid in youtube.get_channel_videos_from_scraper(channel.channel_id, limit):
|
|
||||||
_load_scraped_video(vid, channel)
|
|
||||||
|
|
||||||
|
|
||||||
def update_channel(channel: Channel):
|
def update_channel(channel: Channel):
|
||||||
"""Update a single channel from its RSS feed"""
|
"""Update a single channel from its RSS feed"""
|
||||||
videos = youtube.get_channel_videos_from_feed(channel.channel_id)
|
videos = youtube.get_channel_videos_from_feed(channel.channel_id)
|
||||||
|
@ -135,6 +98,9 @@ def update_channel(channel: Channel):
|
||||||
for vid in videos:
|
for vid in videos:
|
||||||
_load_scraped_video(vid, channel)
|
_load_scraped_video(vid, channel)
|
||||||
|
|
||||||
|
channel.last_update = timezone.now()
|
||||||
|
channel.save()
|
||||||
|
|
||||||
|
|
||||||
def update_channels():
|
def update_channels():
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,8 +6,11 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}{% endblock title %}</title>
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.1.1/css/all.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="{% static 'ucast/css/fontawesome.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'bulma/css/style.min.css' %}">
|
<link rel="stylesheet" href="{% static 'bulma/css/style.min.css' %}">
|
||||||
|
|
||||||
|
<script src="{% static 'ucast/js/htmx.min.js' %}" defer></script>
|
||||||
|
<script src="{% static 'ucast/js/ucast.js' %}" defer></script>
|
||||||
{% block extra_css %}{% endblock extra_css %}
|
{% block extra_css %}{% endblock extra_css %}
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -6,11 +6,26 @@
|
||||||
<h1 class="title">Channels</h1>
|
<h1 class="title">Channels</h1>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<form action="" method="post">
|
{% if form.errors %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>{{ error|escape }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>{{ error|escape }}</strong>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-flex-grow-1">
|
<div class="control is-flex-grow-1">
|
||||||
<input name="channel_str" required class="input" type="text" placeholder="Channel-ID / URL">
|
<input name="channel_str" required class="input" type="text"
|
||||||
|
placeholder="Channel-ID / URL">
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="submit" class="button is-primary">
|
<button type="submit" class="button is-primary">
|
||||||
|
@ -24,13 +39,19 @@
|
||||||
{% for channel in channels %}
|
{% for channel in channels %}
|
||||||
<div class="box is-flex">
|
<div class="box is-flex">
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
<a href="/channel/{{ channel.slug }}">
|
<a href="{% url 'videos' channel.slug %}">
|
||||||
<img class="channel-icon" src="/files/avatar/{{ channel.slug }}.webp?sm">
|
<img class="channel-icon" src="/files/avatar/{{ channel.slug }}.webp?sm">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 is-flex is-flex-direction-column is-flex-grow-1">
|
<div class="ml-3 is-flex is-flex-direction-column is-flex-grow-1">
|
||||||
<a class="subtitle" href="/channel/{{ channel.slug }}">{{ channel.name }}</a>
|
<a class="subtitle" href="/channel/{{ channel.slug }}">{{ channel.name }}</a>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<a href="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
||||||
|
class="button">
|
||||||
|
<i class="fas fa-feed"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="control is-flex-grow-1">
|
<div class="control is-flex-grow-1">
|
||||||
<input class="input" type="text"
|
<input class="input" type="text"
|
||||||
value="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
value="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
||||||
|
|
|
@ -4,91 +4,81 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<h1 class="title">{{ channel.name }}</h1>
|
<div>
|
||||||
|
<h1 class="title">{{ channel.name }}</h1>
|
||||||
|
<p>Last update: {{ channel.last_update }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-user-group"></i> {{ channel.subscribers }}</span>
|
class="fas fa-user-group"></i> {{ channel.subscribers }}</span>
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-video"></i> {{ channel.video_set.count }}</span>
|
class="fas fa-video"></i> {{ videos|length }}</span>
|
||||||
<span class="tag"><i
|
<span class="tag"><i
|
||||||
class="fas fa-database"></i> {{ channel.download_size|filesizeformat }}</span>
|
class="fas fa-database"></i> {{ channel.download_size|filesizeformat }}</span>
|
||||||
<a class="tag" href="{{ channel.get_absolute_url }}" target="_blank"><i
|
<a class="tag" href="{{ channel.get_absolute_url }}" target="_blank"><i
|
||||||
class="fa-brands fa-youtube"></i> {{ channel.channel_id }}</a>
|
class="fa-brands fa-youtube"></i> {{ channel.channel_id }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field has-addons">
|
<form method="post">
|
||||||
<div class="control">
|
|
||||||
<a href="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
|
||||||
class="button">
|
|
||||||
<i class="fas fa-rss"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-success">
|
|
||||||
<i class="fas fa-power-off"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-info">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-danger">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="level">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for video in videos %}
|
|
||||||
<div class="box is-flex">
|
|
||||||
<div class="is-flex">
|
|
||||||
<a href="{{ video.get_absolute_url }}" target="_blank">
|
|
||||||
<img class="video-thumbnail"
|
|
||||||
src="/files/thumbnail/{{ channel.slug }}/{{ video.slug }}.webp?sm">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 is-flex is-flex-direction-column is-flex-grow-1">
|
|
||||||
<a class="subtitle" href="{{ video.get_absolute_url }}"
|
|
||||||
target="_blank">{{ video.title }}</a>
|
|
||||||
|
|
||||||
<div class="tags">
|
|
||||||
<span class="tag"><i
|
|
||||||
class="fas fa-calendar"></i> {{ video.published|date }}</span>
|
|
||||||
<span class="tag"><i
|
|
||||||
class="fas fa-database"></i> {{ video.download_size|filesizeformat }}</span>
|
|
||||||
<a class="tag" href="{{ video.get_absolute_url }}" target="_blank"><i
|
|
||||||
class="fa-brands fa-youtube"></i> {{ video.video_id }}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
{% if video.downloaded %}
|
<div class="control">
|
||||||
<div class="control">
|
<a href="{{ site_url }}/feed/{{ channel.slug }}?key={{ user.get_feed_key }}"
|
||||||
<a class="button is-success"
|
class="button">
|
||||||
href="/files/audio/{{ channel.slug }}/{{ video.slug }}.mp3"
|
<i class="fas fa-rss"></i>
|
||||||
target="_blank">
|
</a>
|
||||||
<i class="fas fa-play"></i>
|
</div>
|
||||||
</a>
|
<div class="control">
|
||||||
</div>
|
{% if channel.active %}
|
||||||
<div class="control">
|
<button type="submit" name="deactivate" class="button is-success">
|
||||||
<button class="button is-danger">
|
<i class="fas fa-power-off"></i>
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{% else %}
|
||||||
{% else %}
|
<button type="submit" name="activate" class="button is-danger">
|
||||||
<div class="control">
|
<i class="fas fa-power-off"></i>
|
||||||
<button class="button is-primary">
|
|
||||||
<i class="fas fa-download"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-info">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" name="delete_channel" class="button is-danger dialog-confirm"
|
||||||
|
confirm-msg="Do you want to delete the channel '{{ channel.name }}' including {{ videos|length }} videos?">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% csrf_token %}
|
||||||
{% endfor %}
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-grid">
|
||||||
|
{% if not videos %}
|
||||||
|
<p>No videos</p>
|
||||||
|
{% endif %}
|
||||||
|
{% include "ucast/videos_items.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if videos.has_previous or videos.has_next %}
|
||||||
|
<noscript>
|
||||||
|
<nav class="pagination is-centered mt-4" role="navigation" aria-label="pagination">
|
||||||
|
{% if videos.has_previous %}
|
||||||
|
<a class="pagination-previous" href="?page={{ videos.previous_page_number }}">Previous</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="pagination-previous" disabled>Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if videos.has_next %}
|
||||||
|
<a class="pagination-next" href="?page={{ videos.next_page_number }}">Next
|
||||||
|
page</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="pagination-previous" disabled>Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</noscript>
|
||||||
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
48
ucast/templates/ucast/videos_items.html
Normal file
48
ucast/templates/ucast/videos_items.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{% for video in videos %}
|
||||||
|
<div class="card video-card"
|
||||||
|
{% if forloop.last and videos.has_next %}
|
||||||
|
hx-get="?page={{ videos.next_page_number }}"
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-swap="afterend"
|
||||||
|
{% endif %}>
|
||||||
|
<a href="{{ video.get_absolute_url }}" target="_blank">
|
||||||
|
<img class="video-thumbnail"
|
||||||
|
src="/files/thumbnail/{{ channel.slug }}/{{ video.slug }}.webp?sm">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="video-card-content is-flex-grow-1">
|
||||||
|
<a href="{{ video.get_absolute_url }}">{{ video.title }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-card-content">
|
||||||
|
|
||||||
|
<div class="level">
|
||||||
|
<span class="tag">
|
||||||
|
<i
|
||||||
|
class="fas fa-calendar"></i> {{ video.published|date:"SHORT_DATE_FORMAT" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-small is-success"
|
||||||
|
href="/files/audio/{{ channel.slug }}/{{ video.slug }}.mp3"
|
||||||
|
target="_blank">
|
||||||
|
<i class="fas fa-play"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ video.id }}">
|
||||||
|
<button type="submit" name="delete_video"
|
||||||
|
class="button is-small is-danger dialog-confirm"
|
||||||
|
confirm-msg="Do you want to delete the video '{{ video.title }}'?">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -28,27 +28,6 @@ def test_download_video(download_dir, rq_queue):
|
||||||
assert os.path.isfile(cf.get_thumbnail(VIDEO_SLUG_INTRO, True))
|
assert os.path.isfile(cf.get_thumbnail(VIDEO_SLUG_INTRO, True))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_import_channel(
|
|
||||||
download_dir, rq_queue, mock_get_video_details, mock_download_audio
|
|
||||||
):
|
|
||||||
# Remove 2 videos from the database so they can be imported
|
|
||||||
Video.objects.get(video_id="ZPxEr4YdWt8").delete()
|
|
||||||
Video.objects.get(video_id="_I5IFObm_-k").delete()
|
|
||||||
|
|
||||||
job = rq_queue.enqueue(download.import_channel, CHANNEL_ID_THETADEV)
|
|
||||||
assert job.is_finished
|
|
||||||
|
|
||||||
mock_download_audio.assert_any_call(
|
|
||||||
"_I5IFObm_-k",
|
|
||||||
download_dir / "ThetaDev" / "20180331_Easter_special_3D_printed_Bunny.mp3",
|
|
||||||
)
|
|
||||||
mock_download_audio.assert_any_call(
|
|
||||||
"ZPxEr4YdWt8",
|
|
||||||
download_dir / "ThetaDev" / "20190602_ThetaDev_Embedded_World_2019.mp3",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_update_channel(
|
def test_update_channel(
|
||||||
download_dir, rq_queue, mock_get_video_details, mock_download_audio
|
download_dir, rq_queue, mock_get_video_details, mock_download_audio
|
||||||
|
|
|
@ -4,7 +4,7 @@ from ucast import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.home),
|
path("", views.home),
|
||||||
path("channel/<str:channel>", views.videos),
|
path("channel/<str:channel>", views.videos, name="videos"),
|
||||||
path("feed/<str:channel>", views.podcast_feed),
|
path("feed/<str:channel>", views.podcast_feed),
|
||||||
path("files/audio/<str:channel>/<str:video>", views.audio),
|
path("files/audio/<str:channel>/<str:video>", views.audio),
|
||||||
path("files/cover/<str:channel>/<str:video>", views.cover),
|
path("files/cover/<str:channel>/<str:video>", views.cover),
|
||||||
|
|
|
@ -8,13 +8,15 @@ from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.contrib.syndication.views import add_domain
|
from django.contrib.syndication.views import add_domain
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.middleware.http import ConditionalGetMiddleware
|
from django.middleware.http import ConditionalGetMiddleware
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.decorators import decorator_from_middleware
|
from django.utils.decorators import decorator_from_middleware
|
||||||
|
|
||||||
from ucast import feed, forms, queue
|
from ucast import feed, forms, queue
|
||||||
from ucast.models import Channel, User, Video
|
from ucast.models import Channel, User, Video
|
||||||
from ucast.service import storage
|
from ucast.service import controller, storage
|
||||||
from ucast.tasks import download
|
from ucast.tasks import download
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,12 +25,22 @@ def home(request: http.HttpRequest):
|
||||||
channels = Channel.objects.all()
|
channels = Channel.objects.all()
|
||||||
|
|
||||||
site_url = add_domain(get_current_site(request).domain, "", request.is_secure())
|
site_url = add_domain(get_current_site(request).domain, "", request.is_secure())
|
||||||
|
form = forms.AddChannelForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = forms.AddChannelForm(request.POST)
|
form = forms.AddChannelForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
channel_str = form.cleaned_data["channel_str"]
|
channel_str = form.cleaned_data["channel_str"]
|
||||||
queue.enqueue(download.import_channel, channel_str)
|
try:
|
||||||
|
channel = controller.create_channel(channel_str)
|
||||||
|
queue.enqueue(download.update_channel, channel)
|
||||||
|
except ValueError:
|
||||||
|
form.add_error("channel_str", "Channel URL invalid")
|
||||||
|
except controller.ChannelAlreadyExistsException:
|
||||||
|
form.add_error("channel_str", "Channel already exists")
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
return http.HttpResponseRedirect(reverse(home))
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
@ -36,7 +48,7 @@ def home(request: http.HttpRequest):
|
||||||
{
|
{
|
||||||
"channels": channels,
|
"channels": channels,
|
||||||
"site_url": site_url,
|
"site_url": site_url,
|
||||||
"add_channel_form": forms.AddChannelForm,
|
"form": form,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,14 +56,49 @@ def home(request: http.HttpRequest):
|
||||||
@login_required
|
@login_required
|
||||||
def videos(request: http.HttpRequest, channel: str):
|
def videos(request: http.HttpRequest, channel: str):
|
||||||
chan = Channel.objects.get(slug=channel)
|
chan = Channel.objects.get(slug=channel)
|
||||||
vids = Video.objects.filter(channel=chan).order_by("-published")
|
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if "activate" in request.POST:
|
||||||
|
chan.active = True
|
||||||
|
chan.save()
|
||||||
|
elif "deactivate" in request.POST:
|
||||||
|
chan.active = False
|
||||||
|
chan.save()
|
||||||
|
elif "delete_video" in request.POST:
|
||||||
|
form = forms.DeleteVideoForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
id = form.cleaned_data["id"]
|
||||||
|
controller.delete_video(id)
|
||||||
|
else:
|
||||||
|
return http.HttpResponseBadRequest()
|
||||||
|
elif "delete_channel" in request.POST:
|
||||||
|
controller.delete_channel(chan.id)
|
||||||
|
return http.HttpResponseRedirect(reverse(home))
|
||||||
|
else:
|
||||||
|
return http.HttpResponseBadRequest()
|
||||||
|
|
||||||
|
return http.HttpResponseRedirect(reverse(videos, args=[channel]))
|
||||||
|
|
||||||
|
vids = Video.objects.filter(channel=chan, downloaded__isnull=False).order_by(
|
||||||
|
"-published"
|
||||||
|
)
|
||||||
site_url = add_domain(get_current_site(request).domain, "", request.is_secure())
|
site_url = add_domain(get_current_site(request).domain, "", request.is_secure())
|
||||||
|
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
videos_p = Paginator(vids, 20)
|
||||||
|
|
||||||
|
template_name = "ucast/videos.html"
|
||||||
|
if request.htmx:
|
||||||
|
template_name = "ucast/videos_items.html"
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"ucast/videos.html",
|
template_name,
|
||||||
{"videos": vids, "channel": chan, "site_url": site_url},
|
{
|
||||||
|
"videos": videos_p.get_page(page_number),
|
||||||
|
"channel": chan,
|
||||||
|
"site_url": site_url,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"bulma",
|
"bulma",
|
||||||
|
"django_htmx",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -109,6 +110,7 @@ MIDDLEWARE = [
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"django_htmx.middleware.HtmxMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "ucast_project.urls"
|
ROOT_URLCONF = "ucast_project.urls"
|
||||||
|
@ -218,6 +220,7 @@ REDIS_QUEUE_TIMEOUT = get_env("REDIS_QUEUE_TIMEOUT", 600)
|
||||||
REDIS_QUEUE_RESULT_TTL = 600
|
REDIS_QUEUE_RESULT_TTL = 600
|
||||||
|
|
||||||
YT_UPDATE_INTERVAL = get_env("YT_UPDATE_INTERVAL", 900)
|
YT_UPDATE_INTERVAL = get_env("YT_UPDATE_INTERVAL", 900)
|
||||||
|
FEED_MAX_ITEMS = get_env("FEED_MAX_ITEMS", 100)
|
||||||
|
|
||||||
INTERNAL_FILES_ROOT = get_env("INTERNAL_FILES_ROOT", "internal_files")
|
INTERNAL_FILES_ROOT = get_env("INTERNAL_FILES_ROOT", "internal_files")
|
||||||
INTERNAL_REDIRECT_HEADER = get_env("INTERNAL_REDIRECT_HEADER", "X-Accel-Redirect")
|
INTERNAL_REDIRECT_HEADER = get_env("INTERNAL_REDIRECT_HEADER", "X-Accel-Redirect")
|
||||||
|
|
Loading…
Reference in a new issue