Compare commits
7 commits
9d4781165f
...
e7b3149962
Author | SHA1 | Date | |
---|---|---|---|
e7b3149962 | |||
52aa14d134 | |||
c18bf3837e | |||
fb0e2ef158 | |||
bf84fc7c78 | |||
5dcbfa638b | |||
d3ad3d012e |
|
@ -4,7 +4,7 @@ type: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Test
|
- name: Test
|
||||||
image: d21d3q/python-poetry:3.10
|
image: thetadev256/ucast-dev
|
||||||
commands:
|
commands:
|
||||||
- poetry install
|
- poetry install
|
||||||
- poetry run invoke lint
|
- poetry run invoke lint
|
||||||
|
|
2
.env
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
UCAST_DEBUG=True
|
||||||
|
UCAST_WORKDIR=_run
|
7
.gitignore
vendored
|
@ -8,6 +8,9 @@ __pycache__
|
||||||
*.egg-info
|
*.egg-info
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
|
|
||||||
|
# JS
|
||||||
|
node_modules
|
||||||
|
|
||||||
# Jupyter
|
# Jupyter
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
@ -18,5 +21,7 @@ __pycache__
|
||||||
|
|
||||||
# Application data
|
# Application data
|
||||||
/_run
|
/_run
|
||||||
.env
|
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
|
# Generated assets
|
||||||
|
/ucast/static/bulma/css
|
||||||
|
|
29
README.md
|
@ -24,5 +24,30 @@ Für ein ansehnliches Ansehen sorgt Bootstrap.
|
||||||
|
|
||||||
### Project aufsetzen
|
### Project aufsetzen
|
||||||
|
|
||||||
1. Python3 + [Poetry](https://python-poetry.org/) dependency manager installieren
|
1. Python3 + Node.js + [Poetry](https://python-poetry.org/) dependency manager +
|
||||||
2. Dependencies mit ``poetry install`` installieren
|
[pnpm](https://pnpm.io/) installieren
|
||||||
|
2. Python-Dependencies mit ``poetry install`` installieren
|
||||||
|
3. Node-Dependencies mit ``pnpm i`` installerien
|
||||||
|
|
||||||
|
### Tasks (Python)
|
||||||
|
|
||||||
|
Ausführen: `invoke <taskname>`
|
||||||
|
|
||||||
|
`test` Unittests ausführen
|
||||||
|
|
||||||
|
`lint` Codequalität/Formatierung überprüfen
|
||||||
|
|
||||||
|
`format` Code mit black formatieren
|
||||||
|
|
||||||
|
`makemigrations` Datenbankmigration erstellen
|
||||||
|
|
||||||
|
`get-cover --vid <YouTube-Video-ID>` YouTube-Thumbnail herunterladen
|
||||||
|
und Coverbilder zum Testen erzeugen (werden unter `ucast/tests/testfiles` abgelegt)
|
||||||
|
|
||||||
|
### Tasks (Node.js)
|
||||||
|
|
||||||
|
Ausführen: `npm run <taskname>`
|
||||||
|
|
||||||
|
`start` Sass-Stylesheets automatisch bei Änderungen kompilieren
|
||||||
|
|
||||||
|
`build` Sass-Stylesheets kompilieren und optimieren
|
||||||
|
|
78
assets/icons/logo.svg
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="68.5mm"
|
||||||
|
height="15.79044mm"
|
||||||
|
viewBox="0 0 68.5 15.79044"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
sodipodi:docname="logo.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
lock-margins="true"
|
||||||
|
fit-margin-top="2"
|
||||||
|
fit-margin-left="2"
|
||||||
|
fit-margin-right="2"
|
||||||
|
fit-margin-bottom="2"
|
||||||
|
inkscape:zoom="9.7594058"
|
||||||
|
inkscape:cx="189.30456"
|
||||||
|
inkscape:cy="45.853201"
|
||||||
|
inkscape:window-width="2516"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid1338"
|
||||||
|
originx="-1.4687504"
|
||||||
|
originy="-18.459564" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-1.4687503,-18.45956)">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#282828;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 67.468749,21.166667 H 56.885416"
|
||||||
|
id="path3041" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#282828;stroke-width:0.98677;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 62.177083,21.444868 V 31.75"
|
||||||
|
id="path3043" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ee0000;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 3.9687503,21.166667 V 27.78125 L 7.9375002,31.75 h 2.6458328 l 3.96875,-3.96875 v -6.614583"
|
||||||
|
id="path3572" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#282828;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 27.781251,21.166667 h -6.614584 l -3.96875,3.96875 v 2.645833 l 3.96875,3.96875 h 6.614584"
|
||||||
|
id="path3687" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#282828;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 30.427084,31.75 v -5.291667 l 5.291667,-5.291666 5.291666,5.291666 V 31.75 v 0"
|
||||||
|
id="path3802" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#282828;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 54.239583,21.166667 h -7.9375 l -2.645834,2.645833 2.645834,2.645833 h 5.291666 L 54.239583,29.104166 51.593749,31.75 h -7.9375"
|
||||||
|
id="path3954" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
107
assets/icons/logo_border.svg
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="68.5mm"
|
||||||
|
height="15.79044mm"
|
||||||
|
viewBox="0 0 68.5 15.79044"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
sodipodi:docname="logo_border.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
lock-margins="true"
|
||||||
|
fit-margin-top="2"
|
||||||
|
fit-margin-left="2"
|
||||||
|
fit-margin-right="2"
|
||||||
|
fit-margin-bottom="2"
|
||||||
|
inkscape:zoom="0.89824763"
|
||||||
|
inkscape:cx="-394.65732"
|
||||||
|
inkscape:cy="200.94681"
|
||||||
|
inkscape:window-width="2516"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer2">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid1338"
|
||||||
|
originx="-1.4687504"
|
||||||
|
originy="-18.459564" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Border">
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke:#ffffff;stroke-linecap:square;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 54.916016,2.206456 v 1 h 0.5 10.583981 0.5 v -1 h -0.5 -10.583981 z"
|
||||||
|
id="path13414" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke:#ffffff;stroke-width:0.494467;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 60.208984,2.461412 V 2.9504073 13.301445 13.79044 h 1 V 13.301445 2.9504073 2.461412 Z"
|
||||||
|
id="path13417" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ee0000;stroke:#ffffff;stroke-linecap:square;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 1.9999997,2.206456 v 0.5 6.822265 l 4.261719,4.261719 h 3.060547 L 13.583984,9.528721 v -6.822265 -0.5 h -1 v 0.5 6.408203 L 8.9082027,12.79044 H 6.6757807 L 2.9999997,9.114659 v -6.408203 -0.5 z"
|
||||||
|
id="path13420" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke:#ffffff;stroke-linecap:square;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 19.490234,2.206456 -4.261718,4.261718 v 3.060547 l 4.261718,4.261719 h 6.822266 0.5 v -1 h -0.5 -6.408203 L 16.228516,9.114659 V 6.882237 l 3.675781,-3.675781 h 6.408203 0.5 v -1 h -0.5 z"
|
||||||
|
id="path13423" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke:#ffffff;stroke-linecap:square;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 34.25,1.999424 -5.791016,5.792969 v 5.498047 0.5 h 1 v -0.5 -5.083984 L 34.25,3.413487 39.041016,8.206456 v 5.083984 0.5 h 1 v -0.5 -5.498047 z"
|
||||||
|
id="path13426" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke:#ffffff;stroke-linecap:square;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 44.626953,2.206456 -3.146484,3.146484 3.146484,3.146484 h 5.291016 l 2.146484,2.144532 -2.146484,2.146484 h -7.730469 -0.5 v 1 h 0.5 8.144531 L 53.478516,10.643956 50.332031,7.499424 H 45.041016 L 42.894531,5.35294 45.041016,3.206456 h 7.730468 0.5 v -1 h -0.5 z"
|
||||||
|
id="path13429" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:label="Main"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-1.4687503,-18.45956)">
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke-linecap:square;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 56.384766,20.666016 v 1 h 0.5 10.583984 0.5 v -1 h -0.5 -10.583984 z"
|
||||||
|
id="path3041" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke-width:0.489566;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="M 61.677734,21.144438 V 21.623788 31.77065 32.25 h 1 v -0.47935 -10.146862 -0.47935 z"
|
||||||
|
id="path3043" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#ee0000;stroke-linecap:square;stroke:none;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 3.46875,20.666016 v 0.5 6.822265 L 7.7304687,32.25 h 3.0605473 l 4.261718,-4.261719 v -6.822265 -0.5 h -1 v 0.5 6.408203 L 10.376953,31.25 H 8.1445313 L 4.46875,27.574219 v -6.408203 -0.5 z"
|
||||||
|
id="path3572" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke-linecap:square;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 20.958984,20.666016 -4.261718,4.261718 v 3.060547 L 20.958984,32.25 h 6.822266 0.5 v -1 h -0.5 -6.408203 l -3.675781,-3.675781 v -2.232422 l 3.675781,-3.675781 h 6.408203 0.5 v -1 h -0.5 z"
|
||||||
|
id="path3687" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke-linecap:square;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 35.71875,20.458984 -5.791016,5.792969 V 31.75 v 0.5 h 1 v -0.5 -5.083984 l 4.791016,-4.792969 4.791016,4.792969 V 31.75 v 0.5 h 1 v -0.5 -5.498047 z"
|
||||||
|
id="path3802" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#282828;stroke-linecap:square;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="m 46.095703,20.666016 -3.146484,3.146484 3.146484,3.146484 h 5.291016 L 53.533203,29.103516 51.386719,31.25 h -7.730469 -0.5 v 1 h 0.5 8.144531 l 3.146485,-3.146484 -3.146485,-3.144532 H 46.509766 L 44.363281,23.8125 46.509766,21.666016 h 7.730468 0.5 v -1 h -0.5 z"
|
||||||
|
id="path3954" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.7 KiB |
78
assets/icons/logo_dark.svg
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="68.5mm"
|
||||||
|
height="15.79044mm"
|
||||||
|
viewBox="0 0 68.5 15.79044"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
sodipodi:docname="logo_dark.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
lock-margins="true"
|
||||||
|
fit-margin-top="2"
|
||||||
|
fit-margin-left="2"
|
||||||
|
fit-margin-right="2"
|
||||||
|
fit-margin-bottom="2"
|
||||||
|
inkscape:zoom="0.34602189"
|
||||||
|
inkscape:cx="-85.254723"
|
||||||
|
inkscape:cy="-293.33405"
|
||||||
|
inkscape:window-width="2516"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid1338"
|
||||||
|
originx="-1.4687504"
|
||||||
|
originy="-18.459564" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-1.4687503,-18.45956)">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 67.468749,21.166667 H 56.885416"
|
||||||
|
id="path3041" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:0.98677;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 62.177083,21.444868 V 31.75"
|
||||||
|
id="path3043" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ee0000;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 3.9687503,21.166667 V 27.78125 L 7.9375002,31.75 h 2.6458328 l 3.96875,-3.96875 v -6.614583"
|
||||||
|
id="path3572" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 27.781251,21.166667 h -6.614584 l -3.96875,3.96875 v 2.645833 l 3.96875,3.96875 h 6.614584"
|
||||||
|
id="path3687" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 30.427084,31.75 v -5.291667 l 5.291667,-5.291666 5.291666,5.291666 V 31.75 v 0"
|
||||||
|
id="path3802" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 54.239583,21.166667 h -7.9375 l -2.645834,2.645833 2.645834,2.645833 h 5.291666 L 54.239583,29.104166 51.593749,31.75 h -7.9375"
|
||||||
|
id="path3954" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
43
assets/icons/yt_icon.svg
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
enable-background="new 0 0 512 512"
|
||||||
|
id="Layer_1"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 508.15335 357.10535"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="YOUTUBE_icon-icons.com_65487.svg"
|
||||||
|
width="508.15335"
|
||||||
|
height="357.10535"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs9" /><sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="1.2304688"
|
||||||
|
inkscape:cx="252.34286"
|
||||||
|
inkscape:cy="163.75873"
|
||||||
|
inkscape:window-width="2516"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" /><g
|
||||||
|
id="g4"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
transform="translate(-3.0903642,-91.894664)"><path
|
||||||
|
d="M 260.4,449 C 203.3,447.2 149,445.8 94.7,443.7 83,443.2 71.1,441.4 59.7,438.7 38.3,433.7 23.5,420.8 15.9,399.7 9.8,382.7 7.6,365.2 6,347.4 2.5,305.6 2.5,263.8 4.2,222 c 1,-23.6 1.6,-47.4 7.9,-70.3 3.8,-13.7 8.4,-27.1 19.5,-37 11.7,-10.5 25.4,-16.8 41,-17.5 42.8,-2.1 85.5,-4.7 128.3,-5.1 57.6,-0.6 115.3,0.2 172.9,1.3 24.9,0.5 50,1.8 74.7,5 22.6,3 39.5,15.6 48.5,37.6 6.9,16.9 9.5,34.6 11,52.6 3.9,45.1 4,90.2 1.8,135.3 -1.1,22.9 -2.2,45.9 -8.7,68.2 -7.4,25.6 -23.1,42.5 -49.3,48.3 -10.2,2.2 -20.8,3 -31.2,3.4 -54.4,1.9 -108.7,3.6 -160.2,5.2 z M 205.1,335.3 c 45.6,-23.6 90.7,-47 136.7,-70.9 -45.9,-24 -91,-47.5 -136.7,-71.4 0,47.7 0,94.6 0,142.3 z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffffff" /></g></svg>
|
After Width: | Height: | Size: 2 KiB |
9
assets/sass/style.sass
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// 1. Import the initial variables
|
||||||
|
@import "../../node_modules/bulma/sass/utilities/initial-variables"
|
||||||
|
|
||||||
|
// 2. Set your own initial variables
|
||||||
|
|
||||||
|
// 3. Import the rest of Bulma
|
||||||
|
@import "../../node_modules/bulma/bulma"
|
||||||
|
|
||||||
|
// 4. Import your stuff here
|
32
deploy/Devcontainer.Dockerfile
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# This has to be built with docker buildx to set the TARGETPLATFORM argument
|
||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# ffmpeg static source (https://johnvansickle.com/ffmpeg/)
|
||||||
|
RUN set -e; \
|
||||||
|
mkdir /build_ffmpeg; \
|
||||||
|
cd /build_ffmpeg; \
|
||||||
|
case "$TARGETPLATFORM" in \
|
||||||
|
"linux/amd64") ffmpeg_arch="amd64";; \
|
||||||
|
"linux/arm64") ffmpeg_arch="arm64";; \
|
||||||
|
"linux/arm/v7") ffmpeg_arch="armhf";; \
|
||||||
|
*) echo "TARGETPLATFORM $TARGETPLATFORM not found"; exit 1 ;;\
|
||||||
|
esac; \
|
||||||
|
wget "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ffmpeg_arch}-static.tar.xz"; \
|
||||||
|
wget "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ffmpeg_arch}-static.tar.xz.md5"; \
|
||||||
|
md5sum -c "ffmpeg-release-${ffmpeg_arch}-static.tar.xz.md5"; \
|
||||||
|
tar Jxf "ffmpeg-release-${ffmpeg_arch}-static.tar.xz"; \
|
||||||
|
mv "ffmpeg-5.0.1-${ffmpeg_arch}-static/ffmpeg" /usr/bin; \
|
||||||
|
cd /; \
|
||||||
|
rm -rf /build_ffmpeg;
|
||||||
|
|
||||||
|
# The cryptography package is written in Rust and not available as a built wheel for armv7
|
||||||
|
# Thats why we need Rust to compile it from source
|
||||||
|
RUN set -e; \
|
||||||
|
if [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \
|
||||||
|
. $HOME/.cargo/env; \
|
||||||
|
fi; \
|
||||||
|
pip install --upgrade pip setuptools poetry; \
|
||||||
|
rm -rf $HOME/.cargo $HOME/.rustup;
|
|
@ -1 +1 @@
|
||||||
ucast/manage.py
|
ucast_project/manage.py
|
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "ucast",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "ThetaDev",
|
||||||
|
"email": "t.testboy@gmail.com"
|
||||||
|
},
|
||||||
|
"description": "YouTube to Podcast converter",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bulma": "^0.9.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"clean-css-cli": "^5.6.0",
|
||||||
|
"postcss": "^8.4.13",
|
||||||
|
"postcss-cli": "^9.1.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.51.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build-clean && npm run build-sass && npm run build-autoprefix && npm run build-cleancss",
|
||||||
|
"build-autoprefix": "postcss --use autoprefixer --map false --output ucast/static/bulma/css/style.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-sass": "sass --style expanded --source-map assets/sass/style.sass ucast/static/bulma/css/style.css",
|
||||||
|
"start": "npm run build-sass -- --watch"
|
||||||
|
}
|
||||||
|
}
|
646
pnpm-lock.yaml
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
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
|
68
poetry.lock
generated
|
@ -157,6 +157,17 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||||
bcrypt = ["bcrypt"]
|
bcrypt = ["bcrypt"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-bulma"
|
||||||
|
version = "0.8.3"
|
||||||
|
description = "Bulma CSS Framework for Django projects"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=2.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "feedparser"
|
name = "feedparser"
|
||||||
version = "6.0.8"
|
version = "6.0.8"
|
||||||
|
@ -239,6 +250,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5, <4"
|
python-versions = ">=3.5, <4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mysqlclient"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "Python interface to MySQL"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -310,6 +329,14 @@ pyyaml = ">=5.1"
|
||||||
toml = "*"
|
toml = "*"
|
||||||
virtualenv = ">=20.0.8"
|
virtualenv = ">=20.0.8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2"
|
||||||
|
version = "2.9.3"
|
||||||
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "py"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
@ -396,6 +423,17 @@ pytest = ">=5.4.0"
|
||||||
docs = ["sphinx", "sphinx-rtd-theme"]
|
docs = ["sphinx", "sphinx-rtd-theme"]
|
||||||
testing = ["django", "django-configurations (>=2.0)"]
|
testing = ["django", "django-configurations (>=2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "0.20.0"
|
||||||
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0"
|
version = "6.0"
|
||||||
|
@ -564,7 +602,7 @@ websockets = "*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "99e2a5970962f1e936da2010b8ec997026f5afe4762af4345da287125e6b7771"
|
content-hash = "8609785f53a44a16f3c5c1d5042ab2627bb198f3c7daa8ea18e55bf1e66c4345"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -802,6 +840,10 @@ django = [
|
||||||
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
|
{file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"},
|
||||||
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
|
{file = "Django-4.0.4.tar.gz", hash = "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"},
|
||||||
]
|
]
|
||||||
|
django-bulma = [
|
||||||
|
{file = "django-bulma-0.8.3.tar.gz", hash = "sha256:b794b4e64f482de77f376451f7cd8b3c8448eb68e5a24c51b9190625a08b0b30"},
|
||||||
|
{file = "django_bulma-0.8.3-py3-none-any.whl", hash = "sha256:0ef6e5c171c2a32010e724a8be61ba6cd0e55ebbd242cf6780560518483c4d00"},
|
||||||
|
]
|
||||||
feedparser = [
|
feedparser = [
|
||||||
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
|
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
|
||||||
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
|
{file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"},
|
||||||
|
@ -840,6 +882,13 @@ mutagen = [
|
||||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||||
]
|
]
|
||||||
|
mysqlclient = [
|
||||||
|
{file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
|
||||||
|
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
|
||||||
|
]
|
||||||
nodeenv = [
|
nodeenv = [
|
||||||
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
||||||
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
||||||
|
@ -900,6 +949,19 @@ pre-commit = [
|
||||||
{file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
|
{file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
|
||||||
{file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
|
{file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
|
||||||
]
|
]
|
||||||
|
psycopg2 = [
|
||||||
|
{file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"},
|
||||||
|
{file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"},
|
||||||
|
{file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"},
|
||||||
|
{file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"},
|
||||||
|
{file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"},
|
||||||
|
{file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"},
|
||||||
|
{file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"},
|
||||||
|
{file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"},
|
||||||
|
{file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"},
|
||||||
|
{file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"},
|
||||||
|
{file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"},
|
||||||
|
]
|
||||||
py = [
|
py = [
|
||||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||||
|
@ -953,6 +1015,10 @@ pytest-django = [
|
||||||
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
||||||
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
|
||||||
]
|
]
|
||||||
|
python-dotenv = [
|
||||||
|
{file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"},
|
||||||
|
{file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"},
|
||||||
|
]
|
||||||
pyyaml = [
|
pyyaml = [
|
||||||
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||||
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||||
|
|
|
@ -5,7 +5,7 @@ description = "YouTube to Podcast converter"
|
||||||
authors = ["Theta-Dev <t.testboy@gmail.com>"]
|
authors = ["Theta-Dev <t.testboy@gmail.com>"]
|
||||||
packages = [
|
packages = [
|
||||||
{ include = "ucast" },
|
{ include = "ucast" },
|
||||||
{ include = "yt2podcast" },
|
{ include = "ucast_project" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
|
@ -21,6 +21,10 @@ wcag-contrast-ratio = "^0.9"
|
||||||
font-source-sans-pro = "^0.0.1"
|
font-source-sans-pro = "^0.0.1"
|
||||||
fonts = "^0.0.3"
|
fonts = "^0.0.3"
|
||||||
bordercrop = "^1.0.0"
|
bordercrop = "^1.0.0"
|
||||||
|
django-bulma = "^0.8.3"
|
||||||
|
python-dotenv = "^0.20.0"
|
||||||
|
psycopg2 = "^2.9.3"
|
||||||
|
mysqlclient = "^2.1.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.1.1"
|
pytest = "^7.1.1"
|
||||||
|
@ -30,7 +34,7 @@ pytest-django = "^4.5.2"
|
||||||
pre-commit = "^2.19.0"
|
pre-commit = "^2.19.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
"ucast-manage" = "ucast.manage:main"
|
"ucast-manage" = "ucast_project.manage:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
57
tasks.py
|
@ -1,40 +1,75 @@
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from invoke import task
|
from invoke import Responder, task
|
||||||
|
|
||||||
from yt2podcast import tests
|
from ucast import tests
|
||||||
from yt2podcast.service import cover, util, youtube
|
from ucast.service import cover, util, youtube
|
||||||
|
|
||||||
os.chdir(Path(__file__).absolute().parent)
|
os.chdir(Path(__file__).absolute().parent)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def test(c):
|
def test(c):
|
||||||
|
"""Run unit tests"""
|
||||||
c.run("pytest", pty=True)
|
c.run("pytest", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def lint(c):
|
def lint(c):
|
||||||
|
"""Check for code quality and formatting"""
|
||||||
c.run("pre-commit run -a", pty=True)
|
c.run("pre-commit run -a", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def format(c):
|
def format(c):
|
||||||
|
"""Format the code with black"""
|
||||||
c.run("pre-commit run black -a", pty=True)
|
c.run("pre-commit run black -a", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def makemigrations(c):
|
def makemigrations(c):
|
||||||
c.run("python manage.py makemigrations yt2podcast")
|
"""Create a new migration that applies the changes made to the data model"""
|
||||||
|
c.run("python manage.py makemigrations ucast")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def collectstatic(c):
|
||||||
|
"""Copy static files into a common folder"""
|
||||||
|
c.run("python manage.py collectstatic --noinput")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def migrate(c):
|
||||||
|
"""Migrate the database"""
|
||||||
|
c.run("python manage.py migrate")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def create_testuser(c):
|
||||||
|
"""Create a test user with the credentials admin:pass"""
|
||||||
|
responder_pwd = Responder(pattern=r"Password.*: ", response="pass\n")
|
||||||
|
responder_yes = Responder(pattern=r"Bypass password validation", response="y\n")
|
||||||
|
|
||||||
|
c.run(
|
||||||
|
"python manage.py createsuperuser --username admin --email admin@example.com",
|
||||||
|
pty=True,
|
||||||
|
watchers=[responder_pwd, responder_yes],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def get_cover(c, vid=""):
|
def get_cover(c, vid=""):
|
||||||
|
"""
|
||||||
|
Download thumbnail image of the YouTube video with the id
|
||||||
|
from the ``--vid`` parameter and create cover images from it.
|
||||||
|
|
||||||
|
The images are stored in the ``ucast/tests/testfiles`` directory.
|
||||||
|
"""
|
||||||
vinfo = youtube.get_video_info(vid)
|
vinfo = youtube.get_video_info(vid)
|
||||||
title = vinfo["fulltitle"]
|
title = vinfo.title
|
||||||
channel_name = vinfo["uploader"]
|
channel_name = vinfo.channel_name
|
||||||
channel_url = vinfo["channel_url"]
|
channel_url = vinfo.channel_url
|
||||||
channel_metadata = youtube.get_channel_metadata(channel_url)
|
channel_metadata = youtube.get_channel_metadata(channel_url)
|
||||||
|
|
||||||
ti = 1
|
ti = 1
|
||||||
|
@ -55,3 +90,11 @@ def get_cover(c, vid=""):
|
||||||
cover.create_cover_file(
|
cover.create_cover_file(
|
||||||
tn_file, av_file, title, channel_name, cover.CoverStyle.BLUR, cv_blur_file
|
tn_file, av_file, title, channel_name, cover.CoverStyle.BLUR, cv_blur_file
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def build_devcontainer(c):
|
||||||
|
c.run(
|
||||||
|
"docker buildx build -t thetadev256/ucast-dev --push \
|
||||||
|
--platform amd64,arm64,armhf -f deploy/Devcontainer.Dockerfile deploy"
|
||||||
|
)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
# coding=utf-8
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class Yt2PodcastConfig(AppConfig):
|
class UcastConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "yt2podcast"
|
name = "ucast"
|
|
@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
||||||
("title", models.CharField(max_length=200)),
|
("title", models.CharField(max_length=200)),
|
||||||
("slug", models.CharField(max_length=209)),
|
("slug", models.CharField(max_length=209)),
|
||||||
("published", models.DateTimeField()),
|
("published", models.DateTimeField()),
|
||||||
("downloaded", models.DateTimeField()),
|
("downloaded", models.DateTimeField(null=True)),
|
||||||
("description", models.TextField()),
|
("description", models.TextField()),
|
||||||
],
|
],
|
||||||
),
|
),
|
|
@ -15,5 +15,5 @@ class Video(models.Model):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
slug = models.CharField(max_length=209)
|
slug = models.CharField(max_length=209)
|
||||||
published = models.DateTimeField()
|
published = models.DateTimeField()
|
||||||
downloaded = models.DateTimeField()
|
downloaded = models.DateTimeField(null=True)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
BIN
ucast/resources/yt_icon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,6 +1,6 @@
|
||||||
# coding=utf-8
|
|
||||||
import enum
|
import enum
|
||||||
import math
|
import math
|
||||||
|
from importlib import resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from colorthief import ColorThief
|
||||||
from fonts.ttf import SourceSansPro
|
from fonts.ttf import SourceSansPro
|
||||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
|
|
||||||
from yt2podcast.service import typ
|
from ucast.service import typ
|
||||||
|
|
||||||
CHAR_ELLIPSIS = "…"
|
CHAR_ELLIPSIS = "…"
|
||||||
COVER_WIDTH = 500
|
COVER_WIDTH = 500
|
||||||
|
@ -24,6 +24,17 @@ class CoverStyle(enum.Enum):
|
||||||
def _split_text(
|
def _split_text(
|
||||||
height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0
|
height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
Split and trim the input text so it can be printed to a certain
|
||||||
|
area of an image.
|
||||||
|
|
||||||
|
:param height: Image area height [px]
|
||||||
|
:param width: Image area width [px]
|
||||||
|
:param text: Input text
|
||||||
|
:param font: Pillow ImageFont
|
||||||
|
:param line_spacing: Line spacing [px]
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if height < font.size:
|
if height < font.size:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -80,6 +91,19 @@ def _draw_text_box(
|
||||||
line_spacing=0,
|
line_spacing=0,
|
||||||
vertical_center=True,
|
vertical_center=True,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Draw a text box to an image. The text gets automatically
|
||||||
|
wrapped and trimmed to fit.
|
||||||
|
|
||||||
|
:param draw: Pillow ImageDraw object
|
||||||
|
:param box: Coordinates of the text box ``(x_tl, y_tl, x_br, y_br)``
|
||||||
|
:param text: Text to be printed
|
||||||
|
:param font: Pillow ImageFont
|
||||||
|
:param color: Text color
|
||||||
|
:param line_spacing: Line spacing [px]
|
||||||
|
:param vertical_center: Center text vertically in the box
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
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
|
||||||
|
@ -96,19 +120,41 @@ def _draw_text_box(
|
||||||
draw.text((x_tl, y_pos), line, color, font)
|
draw.text((x_tl, y_pos), line, color, font)
|
||||||
|
|
||||||
|
|
||||||
def _get_dominant_color(img: Image.Image):
|
def _get_dominant_color(img: Image.Image) -> typ.Color:
|
||||||
|
"""
|
||||||
|
Return the dominant color of an image using the ColorThief library.
|
||||||
|
|
||||||
|
:param img: Pillow Image object
|
||||||
|
:return: dominant color
|
||||||
|
"""
|
||||||
thief = ColorThief.__new__(ColorThief)
|
thief = ColorThief.__new__(ColorThief)
|
||||||
thief.image = img
|
thief.image = img
|
||||||
return thief.get_color()
|
return thief.get_color()
|
||||||
|
|
||||||
|
|
||||||
def _interpolate_color(color_from: typ.Color, color_to: typ.Color, interval: int):
|
def _interpolate_color(color_from: typ.Color, color_to: typ.Color, steps: int):
|
||||||
det_co = [(t - f) / interval for f, t in zip(color_from, color_to)]
|
"""
|
||||||
for i in range(interval):
|
Return a generator providing colors within the given range. Useful to create
|
||||||
|
gradients.
|
||||||
|
|
||||||
|
:param color_from: Starting color
|
||||||
|
:param color_to: Ending color
|
||||||
|
:param steps: Number of steps
|
||||||
|
:return: Generator providing the colors
|
||||||
|
"""
|
||||||
|
det_co = [(t - f) / steps for f, t in zip(color_from, color_to)]
|
||||||
|
for i in range(steps):
|
||||||
yield [round(f + det * i) for f, det in zip(color_from, det_co)]
|
yield [round(f + det * i) for f, det in zip(color_from, det_co)]
|
||||||
|
|
||||||
|
|
||||||
def _get_text_color(bg_color) -> typ.Color:
|
def _get_text_color(bg_color) -> typ.Color:
|
||||||
|
"""
|
||||||
|
Return the text color (black or white) with the largest contrast
|
||||||
|
to a given background color.
|
||||||
|
|
||||||
|
:param bg_color: Background color
|
||||||
|
:return: Text color
|
||||||
|
"""
|
||||||
color_decimal = tuple([c / 255 for c in bg_color])
|
color_decimal = tuple([c / 255 for c in bg_color])
|
||||||
c_blk = wcag_contrast_ratio.rgb((0, 0, 0), color_decimal)
|
c_blk = wcag_contrast_ratio.rgb((0, 0, 0), color_decimal)
|
||||||
c_wht = wcag_contrast_ratio.rgb((1, 1, 1), color_decimal)
|
c_wht = wcag_contrast_ratio.rgb((1, 1, 1), color_decimal)
|
||||||
|
@ -123,17 +169,26 @@ def _get_baseimage(
|
||||||
bottom_color: typ.Color,
|
bottom_color: typ.Color,
|
||||||
style: CoverStyle,
|
style: CoverStyle,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Return the background image for the cover.
|
||||||
|
|
||||||
|
:param thumbnail: Thumbnail image object
|
||||||
|
:param top_color: Top color of the thumbnail image
|
||||||
|
:param bottom_color: Bottom color of the thumbnail image
|
||||||
|
:param style: Style of the cover image
|
||||||
|
:return: Base image
|
||||||
|
"""
|
||||||
|
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
|
||||||
|
|
||||||
if style == CoverStyle.BLUR:
|
if style == CoverStyle.BLUR:
|
||||||
ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width)
|
ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width)
|
||||||
ctn_l = int((ctn_width - COVER_WIDTH) / 2)
|
ctn_x_left = int((ctn_width - COVER_WIDTH) / 2)
|
||||||
ctn_r = ctn_width - ctn_l
|
|
||||||
cover = (
|
ctn = thumbnail.resize(
|
||||||
thumbnail.resize((ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS)
|
(ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS
|
||||||
.crop((ctn_l, 0, ctn_r, COVER_WIDTH))
|
).filter(ImageFilter.GaussianBlur(20))
|
||||||
.filter(ImageFilter.GaussianBlur(20))
|
cover.paste(ctn, (-ctn_x_left, 0))
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH))
|
|
||||||
cover_draw = ImageDraw.Draw(cover)
|
cover_draw = ImageDraw.Draw(cover)
|
||||||
|
|
||||||
# Draw background gradient
|
# Draw background gradient
|
||||||
|
@ -145,14 +200,13 @@ def _get_baseimage(
|
||||||
return cover
|
return cover
|
||||||
|
|
||||||
|
|
||||||
def _create_cover_image(
|
def _resize_thumbnail(thumbnail: Image.Image) -> Image.Image:
|
||||||
thumbnail: Image.Image,
|
"""
|
||||||
avatar: Optional[Image.Image],
|
Scale thumbnail image down to cover size and remove black bars
|
||||||
title: str,
|
|
||||||
channel: str,
|
:param thumbnail: Thumbnail image object
|
||||||
style: CoverStyle,
|
:return: Resized thumbnail image object
|
||||||
) -> Image.Image:
|
"""
|
||||||
# Remove black bars from thumbnail
|
|
||||||
thumbnail = bordercrop.crop(
|
thumbnail = bordercrop.crop(
|
||||||
thumbnail,
|
thumbnail,
|
||||||
MINIMUM_ROWS=int(thumbnail.height * 0.1),
|
MINIMUM_ROWS=int(thumbnail.height * 0.1),
|
||||||
|
@ -163,30 +217,31 @@ def _create_cover_image(
|
||||||
tn_resize_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
|
tn_resize_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height)
|
||||||
tn_16_9_height = int(COVER_WIDTH / 16 * 9)
|
tn_16_9_height = int(COVER_WIDTH / 16 * 9)
|
||||||
tn_height = min(tn_resize_height, tn_16_9_height)
|
tn_height = min(tn_resize_height, tn_16_9_height)
|
||||||
tn_crop_t = int((tn_resize_height - tn_height) / 2)
|
tn_crop_y_top = int((tn_resize_height - tn_height) / 2)
|
||||||
tn_crop_b = tn_resize_height - tn_crop_t
|
tn_crop_y_bottom = tn_resize_height - tn_crop_y_top
|
||||||
tn = thumbnail.resize(
|
|
||||||
|
return thumbnail.resize(
|
||||||
(COVER_WIDTH, tn_resize_height), Image.Resampling.LANCZOS
|
(COVER_WIDTH, tn_resize_height), Image.Resampling.LANCZOS
|
||||||
).crop((0, tn_crop_t, COVER_WIDTH, tn_crop_b))
|
).crop((0, tn_crop_y_top, COVER_WIDTH, tn_crop_y_bottom))
|
||||||
|
|
||||||
# Get dominant colors from the top and bottom 20% of the thumbnail image
|
|
||||||
top_part = tn.crop((0, 0, COVER_WIDTH, int(tn_height * 0.2)))
|
|
||||||
bottom_part = tn.crop((0, int(tn_height * 0.8), COVER_WIDTH, tn_height))
|
|
||||||
top_color = _get_dominant_color(top_part)
|
|
||||||
bottom_color = _get_dominant_color(bottom_part)
|
|
||||||
|
|
||||||
cover = _get_baseimage(thumbnail, top_color, bottom_color, style)
|
def _draw_text_avatar(
|
||||||
|
cover: Image.Image,
|
||||||
|
avatar: Optional[Image.Image],
|
||||||
|
title: str,
|
||||||
|
channel: str,
|
||||||
|
top_color: typ.Color,
|
||||||
|
bottom_color: typ.Color,
|
||||||
|
):
|
||||||
cover_draw = ImageDraw.Draw(cover)
|
cover_draw = ImageDraw.Draw(cover)
|
||||||
|
|
||||||
# Insert thumbnail image in the middle
|
|
||||||
tn_margin = int((COVER_WIDTH - tn_height) / 2)
|
|
||||||
tn_16_9_margin = int((COVER_WIDTH - tn_16_9_height) / 2)
|
|
||||||
cover.paste(tn, (0, tn_margin))
|
|
||||||
|
|
||||||
# Add channel avatar
|
# Add channel avatar
|
||||||
avt_margin = 0
|
avt_margin = 0
|
||||||
avt_size = 0
|
avt_size = 0
|
||||||
|
|
||||||
|
tn_16_9_height = int(COVER_WIDTH / 16 * 9)
|
||||||
|
tn_16_9_margin = int((COVER_WIDTH - tn_16_9_height) / 2)
|
||||||
|
|
||||||
if avatar:
|
if avatar:
|
||||||
avt_margin = int(tn_16_9_margin * 0.05)
|
avt_margin = int(tn_16_9_margin * 0.05)
|
||||||
avt_size = tn_16_9_margin - 2 * avt_margin
|
avt_size = tn_16_9_margin - 2 * avt_margin
|
||||||
|
@ -236,22 +291,90 @@ def _create_cover_image(
|
||||||
text_line_space,
|
text_line_space,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_cover_image(
|
||||||
|
thumbnail: Image.Image,
|
||||||
|
avatar: Optional[Image.Image],
|
||||||
|
title: str,
|
||||||
|
channel: str,
|
||||||
|
style: CoverStyle,
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Create a cover image from video metadata and thumbnail
|
||||||
|
|
||||||
|
:param thumbnail: Thumbnail image object
|
||||||
|
:param avatar: Creator avatar image object
|
||||||
|
:param title: Video title
|
||||||
|
:param channel: Channel name
|
||||||
|
:param style: Style of cover image
|
||||||
|
:return: Cover image
|
||||||
|
"""
|
||||||
|
tn = _resize_thumbnail(thumbnail)
|
||||||
|
|
||||||
|
# Get dominant colors from the top and bottom 20% of the thumbnail image
|
||||||
|
top_part = tn.crop((0, 0, COVER_WIDTH, int(tn.height * 0.2)))
|
||||||
|
bottom_part = tn.crop((0, int(tn.height * 0.8), COVER_WIDTH, tn.height))
|
||||||
|
top_color = _get_dominant_color(top_part)
|
||||||
|
bottom_color = _get_dominant_color(bottom_part)
|
||||||
|
|
||||||
|
cover = _get_baseimage(tn, top_color, bottom_color, style)
|
||||||
|
|
||||||
|
# Insert thumbnail image in the middle
|
||||||
|
tn_margin = int((COVER_WIDTH - tn.height) / 2)
|
||||||
|
cover.paste(tn, (0, tn_margin))
|
||||||
|
|
||||||
|
_draw_text_avatar(cover, avatar, title, channel, top_color, bottom_color)
|
||||||
|
|
||||||
|
return cover
|
||||||
|
|
||||||
|
|
||||||
|
def _create_blank_cover_image(
|
||||||
|
avatar: Optional[Image.Image], title: str, channel: str
|
||||||
|
) -> Image.Image:
|
||||||
|
bg_color = (16, 16, 16)
|
||||||
|
cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH), bg_color)
|
||||||
|
|
||||||
|
yt_icon_path = resources.path("ucast.resources", "yt_icon.png")
|
||||||
|
yt_icon = Image.open(yt_icon_path)
|
||||||
|
yt_icon_x_left = int((COVER_WIDTH - yt_icon.width) / 2)
|
||||||
|
yt_icon_y_top = int((COVER_WIDTH - yt_icon.height) / 2)
|
||||||
|
cover.paste(yt_icon, (yt_icon_x_left, yt_icon_y_top))
|
||||||
|
|
||||||
|
_draw_text_avatar(cover, avatar, title, channel, bg_color, bg_color)
|
||||||
|
|
||||||
return cover
|
return cover
|
||||||
|
|
||||||
|
|
||||||
def create_cover_file(
|
def create_cover_file(
|
||||||
thumbnail_path: Path,
|
thumbnail_path: Optional[Path],
|
||||||
avatar_path: Optional[Path],
|
avatar_path: Optional[Path],
|
||||||
title: str,
|
title: str,
|
||||||
channel: str,
|
channel: str,
|
||||||
style: CoverStyle,
|
style: CoverStyle,
|
||||||
cover_path: Path,
|
cover_path: Path,
|
||||||
):
|
):
|
||||||
thumbnail = Image.open(thumbnail_path)
|
"""
|
||||||
|
Create a cover image from video metadata and thumbnail
|
||||||
|
and save it to disk.
|
||||||
|
|
||||||
|
:param thumbnail_path: Path of thumbnail image
|
||||||
|
:param avatar_path: Path of avatar image
|
||||||
|
:param title: Video title
|
||||||
|
:param channel: Channel name
|
||||||
|
:param style: Style of cover image
|
||||||
|
:param cover_path: Save path of cover image
|
||||||
|
"""
|
||||||
|
thumbnail = None
|
||||||
|
if thumbnail_path:
|
||||||
|
thumbnail = Image.open(thumbnail_path)
|
||||||
|
|
||||||
avatar = None
|
avatar = None
|
||||||
if avatar_path:
|
if avatar_path:
|
||||||
avatar = Image.open(avatar_path)
|
avatar = Image.open(avatar_path)
|
||||||
|
|
||||||
cvr = _create_cover_image(thumbnail, avatar, title, channel, style)
|
if thumbnail:
|
||||||
|
cvr = _create_cover_image(thumbnail, avatar, title, channel, style)
|
||||||
|
else:
|
||||||
|
cvr = _create_blank_cover_image(avatar, title, channel)
|
||||||
|
|
||||||
cvr.save(cover_path)
|
cvr.save(cover_path)
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
Color = Tuple[int, int, int]
|
Color = Tuple[int, int, int]
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
145
ucast/service/youtube.py
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from scrapetube import scrapetube
|
||||||
|
from yt_dlp import YoutubeDL
|
||||||
|
|
||||||
|
from ucast.service import util
|
||||||
|
|
||||||
|
|
||||||
|
class VideoInfo:
|
||||||
|
"""Mapping of YoutubeDL's video information"""
|
||||||
|
|
||||||
|
def __init__(self, info: dict):
|
||||||
|
self._info = info
|
||||||
|
|
||||||
|
self.id = info["id"]
|
||||||
|
self.title = info["title"]
|
||||||
|
self.description = info["description"]
|
||||||
|
self.channel_id = info["channel_id"]
|
||||||
|
self.channel_name = info["uploader"]
|
||||||
|
self.duration = info["duration"]
|
||||||
|
self.published = self.__approx_published_time(
|
||||||
|
datetime.strptime(info["upload_date"], "%Y%m%d")
|
||||||
|
)
|
||||||
|
self.thumbnails = info["thumbnails"]
|
||||||
|
self.is_currently_live = bool(info.get("is_live"))
|
||||||
|
self.is_livestream = info.get("is_live") or info.get("was_live")
|
||||||
|
self.is_short = self.duration <= 60 and info["width"] < info["height"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __approx_published_time(time_in: datetime) -> datetime:
|
||||||
|
"""
|
||||||
|
Assume that a video published on the current day is published now.
|
||||||
|
Eventually add an option to get the exact upload time from Google's API.
|
||||||
|
|
||||||
|
:param time_in:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
if time_in.date() == now.date():
|
||||||
|
return now
|
||||||
|
return time_in
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title} ({self.id})"
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbnailNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def download_thumbnail(vinfo: VideoInfo, download_path):
|
||||||
|
"""
|
||||||
|
Download the thumbnail image of a YouTube video and save it at the given filepath.
|
||||||
|
Does not add the correct file ending (jpg or webp), we are converting it with
|
||||||
|
Pillow anyway.
|
||||||
|
|
||||||
|
:param vinfo: Video info (from ``get_video_info()``)
|
||||||
|
:param download_path: Path of the thumbnail file
|
||||||
|
:raise ThumbnailNotFoundError: if no thumbnail could be found (YT returned 404)
|
||||||
|
"""
|
||||||
|
for tn in sorted(vinfo.thumbnails, key=itemgetter("preference"), reverse=True):
|
||||||
|
url = tn["url"]
|
||||||
|
print(f"downloading thumbnail {url}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
util.download_file(url, download_path)
|
||||||
|
return
|
||||||
|
except requests.HTTPError:
|
||||||
|
print(f"downloading thumbnail {url} failed")
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ThumbnailNotFoundError(f"could not find thumbnail for video {vinfo}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_info(video_id) -> VideoInfo:
|
||||||
|
with YoutubeDL() as ydl:
|
||||||
|
info = ydl.extract_info(video_id, download=False)
|
||||||
|
return VideoInfo(info)
|
||||||
|
|
||||||
|
|
||||||
|
def download_video(video_id, download_path, sponsorblock=False) -> VideoInfo:
|
||||||
|
ydl_params = {
|
||||||
|
"format": "bestaudio",
|
||||||
|
"postprocessors": [
|
||||||
|
{"key": "FFmpegExtractAudio", "preferredcodec": "mp3"},
|
||||||
|
],
|
||||||
|
"outtmpl": str(download_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
if sponsorblock:
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
ydl_params["postprocessors"].extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "SponsorBlock",
|
||||||
|
"categories": ["sponsor"],
|
||||||
|
"when": "after_filter",
|
||||||
|
},
|
||||||
|
{"key": "ModifyChapters", "remove_sponsor_segments": ["sponsor"]},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with YoutubeDL(ydl_params) as ydl:
|
||||||
|
# extract_info downloads the video and returns its metadata
|
||||||
|
info = ydl.extract_info(video_id)
|
||||||
|
return VideoInfo(info)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChannelMetadata:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
avatar_url: str
|
||||||
|
|
||||||
|
|
||||||
|
def channel_url_from_id(channel_id: str) -> str:
|
||||||
|
return "https://www.youtube.com/channel/" + channel_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_metadata(channel_url: str) -> ChannelMetadata:
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers[
|
||||||
|
"User-Agent"
|
||||||
|
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
|
||||||
|
(KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36"
|
||||||
|
|
||||||
|
url = f"{channel_url}/videos?view=0&flow=grid"
|
||||||
|
|
||||||
|
html = scrapetube.get_initial_data(session, url)
|
||||||
|
data = json.loads(
|
||||||
|
scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}"
|
||||||
|
)
|
||||||
|
metadata = data["metadata"]["channelMetadataRenderer"]
|
||||||
|
|
||||||
|
channel_id = metadata["externalId"]
|
||||||
|
name = metadata["title"]
|
||||||
|
description = metadata["description"]
|
||||||
|
avatar = metadata["avatar"]["thumbnails"][0]["url"]
|
||||||
|
|
||||||
|
return ChannelMetadata(channel_id, name, description, avatar)
|
3
ucast/tests/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
DIR_TESTFILES = files("ucast.tests.testfiles")
|
|
@ -1,4 +1,3 @@
|
||||||
# coding=utf-8
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -7,8 +6,8 @@ import pytest
|
||||||
from fonts.ttf import SourceSansPro
|
from fonts.ttf import SourceSansPro
|
||||||
from PIL import Image, ImageChops, ImageFont
|
from PIL import Image, ImageChops, ImageFont
|
||||||
|
|
||||||
from yt2podcast import tests
|
from ucast import tests
|
||||||
from yt2podcast.service import cover, typ
|
from ucast.service import cover, typ
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -50,7 +49,7 @@ def test_split_text(height: int, width: int, text: str, expect: List[str]):
|
||||||
"file_name,color",
|
"file_name,color",
|
||||||
[
|
[
|
||||||
("t1.webp", (63, 63, 62)),
|
("t1.webp", (63, 63, 62)),
|
||||||
("t2.webp", (17, 14, 15)),
|
("t2.webp", (22, 20, 20)),
|
||||||
("t3.webp", (54, 24, 28)),
|
("t3.webp", (54, 24, 28)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -118,6 +117,47 @@ def test_create_cover_image(
|
||||||
|
|
||||||
cv_image = cover._create_cover_image(tn_image, av_image, title, channel, style)
|
cv_image = cover._create_cover_image(tn_image, av_image, title, channel, style)
|
||||||
|
|
||||||
|
assert cv_image.width == cover.COVER_WIDTH
|
||||||
|
assert cv_image.height == cover.COVER_WIDTH
|
||||||
|
|
||||||
|
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||||
|
assert diff.getbbox() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_cover_image_noavatar():
|
||||||
|
tn_file = tests.DIR_TESTFILES / "thumbnail" / "t1.webp"
|
||||||
|
expected_cv_file = tests.DIR_TESTFILES / "cover" / "c1_noavatar.png"
|
||||||
|
|
||||||
|
tn_image = Image.open(tn_file)
|
||||||
|
expected_cv_image = Image.open(expected_cv_file)
|
||||||
|
|
||||||
|
cv_image = cover._create_cover_image(
|
||||||
|
tn_image,
|
||||||
|
None,
|
||||||
|
"ThetaDev @ Embedded World 2019",
|
||||||
|
"ThetaDev",
|
||||||
|
cover.CoverStyle.CLASSIC,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cv_image.width == cover.COVER_WIDTH
|
||||||
|
assert cv_image.height == cover.COVER_WIDTH
|
||||||
|
|
||||||
|
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||||
|
assert diff.getbbox() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_blank_cover_image():
|
||||||
|
av_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
||||||
|
expected_cv_file = tests.DIR_TESTFILES / "cover" / "blank.png"
|
||||||
|
|
||||||
|
av_image = Image.open(av_file)
|
||||||
|
expected_cv_image = Image.open(expected_cv_file)
|
||||||
|
|
||||||
|
cv_image = cover._create_blank_cover_image(av_image, "missingno", "ThetaDev")
|
||||||
|
|
||||||
|
assert cv_image.width == cover.COVER_WIDTH
|
||||||
|
assert cv_image.height == cover.COVER_WIDTH
|
||||||
|
|
||||||
diff = ImageChops.difference(cv_image, expected_cv_image)
|
diff = ImageChops.difference(cv_image, expected_cv_image)
|
||||||
assert diff.getbbox() is None
|
assert diff.getbbox() is None
|
||||||
|
|
24
ucast/tests/test_util.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
from ucast import tests
|
||||||
|
from ucast.service import util
|
||||||
|
|
||||||
|
TEST_FILE_URL = "https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_file():
|
||||||
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
|
tmpdir = Path(tmpdir_o.name)
|
||||||
|
download_file = tmpdir / "download.jpg"
|
||||||
|
expected_tn_file = tests.DIR_TESTFILES / "avatar" / "a1.jpg"
|
||||||
|
|
||||||
|
util.download_file(TEST_FILE_URL, download_file)
|
||||||
|
|
||||||
|
downloaded_avatar = Image.open(download_file)
|
||||||
|
expected_avatar = Image.open(expected_tn_file)
|
||||||
|
|
||||||
|
diff = ImageChops.difference(downloaded_avatar, expected_avatar)
|
||||||
|
assert diff.getbbox() is None
|
129
ucast/tests/test_youtube.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
from ucast import tests
|
||||||
|
from ucast.service import youtube
|
||||||
|
|
||||||
|
VIDEO_ID_SINTEL = "eRsGyueVLvQ"
|
||||||
|
VIDEO_ID_SHORT = "lcQZ6YwQHiw"
|
||||||
|
VIDEO_ID_PERSUASION = "DWjFW7Yq1fA"
|
||||||
|
|
||||||
|
CHANNEL_ID_THETADEV = "UCGiJh0NZ52wRhYKYnuZI08Q"
|
||||||
|
CHANNEL_ID_BLENDER = "UCSMOQeBJ2RAnuFungnQOxLg"
|
||||||
|
CHANNEL_URL_BLENDER = "https://www.youtube.com/c/BlenderFoundation"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def video_info() -> youtube.VideoInfo:
|
||||||
|
return youtube.get_video_info(VIDEO_ID_SINTEL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_thumbnail(video_info):
|
||||||
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
|
tmpdir = Path(tmpdir_o.name)
|
||||||
|
tn_file = tmpdir / "thumbnail"
|
||||||
|
expected_tn_file = tests.DIR_TESTFILES / "thumbnail" / "t2.webp"
|
||||||
|
|
||||||
|
youtube.download_thumbnail(video_info, tn_file)
|
||||||
|
|
||||||
|
tn = Image.open(tn_file)
|
||||||
|
expected_tn = Image.open(expected_tn_file)
|
||||||
|
|
||||||
|
diff = ImageChops.difference(tn, expected_tn)
|
||||||
|
assert diff.getbbox() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_video_info(video_info):
|
||||||
|
assert video_info.id == VIDEO_ID_SINTEL
|
||||||
|
assert video_info.title == "Sintel - Open Movie by Blender Foundation"
|
||||||
|
assert video_info.channel_id == "UCSMOQeBJ2RAnuFungnQOxLg"
|
||||||
|
assert (
|
||||||
|
video_info.description
|
||||||
|
== """Help us making Free/Open Movies: https://cloud.blender.org/join
|
||||||
|
|
||||||
|
"Sintel" is an independently produced short film, initiated by the Blender Foundation \
|
||||||
|
as a means to further improve and validate the free/open source 3D creation suite \
|
||||||
|
Blender. With initial funding provided by 1000s of donations via the internet \
|
||||||
|
community, it has \
|
||||||
|
again proven to be a viable development model for both open 3D technology as for \
|
||||||
|
independent animation film.
|
||||||
|
This 15 minute film has been realized in the studio of the Amsterdam Blender \
|
||||||
|
Institute, by an international team of artists and developers. In addition to \
|
||||||
|
that, several crucial technical and creative targets have been realized online, \
|
||||||
|
by developers and artists and teams all over the world.
|
||||||
|
|
||||||
|
www.sintel.org"""
|
||||||
|
)
|
||||||
|
assert video_info.duration == 888
|
||||||
|
assert not video_info.is_currently_live
|
||||||
|
assert not video_info.is_livestream
|
||||||
|
assert not video_info.is_short
|
||||||
|
assert video_info.published == datetime(2010, 9, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_video_info_short():
|
||||||
|
vinfo = youtube.get_video_info(VIDEO_ID_SHORT)
|
||||||
|
assert vinfo.id == VIDEO_ID_SHORT
|
||||||
|
assert (
|
||||||
|
vinfo.title
|
||||||
|
== "Small pink flowers | #shorts | Free Stock Video | \
|
||||||
|
creative commons short videos | creative #short"
|
||||||
|
)
|
||||||
|
assert not vinfo.is_currently_live
|
||||||
|
assert not vinfo.is_livestream
|
||||||
|
assert vinfo.is_short
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_video():
|
||||||
|
tmpdir_o = tempfile.TemporaryDirectory()
|
||||||
|
tmpdir = Path(tmpdir_o.name)
|
||||||
|
download_file = tmpdir / "download.mp3"
|
||||||
|
|
||||||
|
vinfo = youtube.download_video(VIDEO_ID_PERSUASION, download_file)
|
||||||
|
assert vinfo.id == VIDEO_ID_PERSUASION
|
||||||
|
assert vinfo.title == "Persuasion (Instrumental) – RYYZN (No Copyright Music)"
|
||||||
|
assert vinfo.duration == 100
|
||||||
|
|
||||||
|
# Check with ffmpeg if the audio file is valid
|
||||||
|
res = subprocess.run(
|
||||||
|
["ffmpeg", "-i", str(download_file)],
|
||||||
|
capture_output=True,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
assert "Stream #0:0: Audio: mp3" in res.stderr
|
||||||
|
|
||||||
|
match = re.search(r"Duration: (\d{2}:\d{2}:\d{2})", res.stderr)
|
||||||
|
assert match[1] == "00:01:40"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"channel_url,channel_id,name,avatar_url",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
youtube.channel_url_from_id(CHANNEL_ID_THETADEV),
|
||||||
|
CHANNEL_ID_THETADEV,
|
||||||
|
"ThetaDev",
|
||||||
|
"https://yt3.ggpht.com/ytc/AKedOLSnFfmpibLLoqyaYdsF6bJ-zaLPzomII__FrJve1w=s900-c-k-c0x00ffffff-no-rj", # noqa: E501
|
||||||
|
),
|
||||||
|
(
|
||||||
|
CHANNEL_URL_BLENDER,
|
||||||
|
CHANNEL_ID_BLENDER,
|
||||||
|
"Blender",
|
||||||
|
"https://yt3.ggpht.com/ytc/AKedOLT_31fFSD3FWEBnHZnyZeJx-GPHJwYCQKcEpaq8NQ=s900-c-k-c0x00ffffff-no-rj", # noqa: E501
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_channel_metadata(
|
||||||
|
channel_url: str, channel_id: str, name: str, avatar_url: str
|
||||||
|
):
|
||||||
|
metadata = youtube.get_channel_metadata(channel_url)
|
||||||
|
assert metadata.id == channel_id
|
||||||
|
assert metadata.name == name
|
||||||
|
assert metadata.avatar_url == avatar_url
|
||||||
|
assert metadata.description
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
BIN
ucast/tests/testfiles/cover/blank.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
ucast/tests/testfiles/cover/c1_blur.png
Normal file
After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
BIN
ucast/tests/testfiles/cover/c1_noavatar.png
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
ucast/tests/testfiles/cover/c2_blur.png
Normal file
After Width: | Height: | Size: 243 KiB |
BIN
ucast/tests/testfiles/cover/c2_classic.png
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
ucast/tests/testfiles/cover/c3_blur.png
Normal file
After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
10
ucast/tests/testfiles/sources.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
### Quellen der Thumbnails/Avatarbilder zum Testen
|
||||||
|
|
||||||
|
- a1/t1: [ThetaDev @ Embedded World 2019](https://www.youtube.com/watch?v=ZPxEr4YdWt8), by [ThetaDev](https://www.youtube.com/channel/UCGiJh0NZ52wRhYKYnuZI08Q) (CC-BY)
|
||||||
|
- a2/t2: [Sintel - Open Movie by Blender Foundation](https://www.youtube.com/watch?v=eRsGyueVLvQ), by [Blender](https://www.youtube.com/c/BlenderFoundation) (CC-BY)
|
||||||
|
- a3/t3: [Systemabsturz Teaser zur DiVOC bb3](https://www.youtube.com/watch?v=uFqgQ35wyYY), by [media.ccc.de](https://www.youtube.com/channel/UC2TXq_t06Hjdr2g_KdKpHQg) (CC-BY)
|
||||||
|
|
||||||
|
### Weitere Testvideos
|
||||||
|
|
||||||
|
- [Persuasion (Instrumental) – RYYZN (No Copyright Music)](https://www.youtube.com/watch?v=DWjFW7Yq1fA), by [RYYZN](https://soundcloud.com/ryyzn) (CC-BY)
|
||||||
|
- [Small pink flowers | #shorts | Free Stock Video](https://www.youtube.com/watch?v=lcQZ6YwQHiw), by [Shahzaib Hassan](https://www.youtube.com/channel/UCmLTTbctUZobNQrr8RtX8uQ), (CC-BY)
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
BIN
ucast/tests/testfiles/thumbnail/t2.webp
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
0
ucast_project/__init__.py
Normal file
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings")
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
|
@ -6,7 +6,7 @@ import sys
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
|
@ -10,32 +10,82 @@ For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from importlib import resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
import dotenv
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent / "_run"
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
VAR_PREFIX = "UCAST_"
|
||||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
|
||||||
|
|
||||||
|
def get_env(name, default=None):
|
||||||
|
return os.environ.get(VAR_PREFIX + name, default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_path(name, default=None):
|
||||||
|
raw_env = get_env(name)
|
||||||
|
if not raw_env:
|
||||||
|
return default
|
||||||
|
return Path(raw_env).absolute()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_dotenv() -> Path:
|
||||||
|
"""
|
||||||
|
Look for a .env file in the current working directory or
|
||||||
|
its parent directories.
|
||||||
|
|
||||||
|
The directory containing the .env file becomes the application
|
||||||
|
working directory, if no ``UCAST_WORKDIR`` environment variable
|
||||||
|
is present.
|
||||||
|
|
||||||
|
:return: Application working directory
|
||||||
|
"""
|
||||||
|
dotenv_path = dotenv.find_dotenv()
|
||||||
|
default_workdir = Path().resolve()
|
||||||
|
|
||||||
|
if dotenv_path:
|
||||||
|
dotenv.load_dotenv(dotenv_path)
|
||||||
|
print(f"Loaded config from envfile at {dotenv_path}")
|
||||||
|
default_workdir = Path(dotenv_path).resolve().parent
|
||||||
|
|
||||||
|
os.chdir(default_workdir)
|
||||||
|
|
||||||
|
env_workdir = get_env("WORKDIR")
|
||||||
|
if env_workdir:
|
||||||
|
env_workdir_path = Path(env_workdir).resolve()
|
||||||
|
os.makedirs(env_workdir_path, exist_ok=True)
|
||||||
|
os.chdir(env_workdir_path)
|
||||||
|
return env_workdir_path
|
||||||
|
|
||||||
|
return default_workdir
|
||||||
|
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = _load_dotenv()
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "django-insecure-$b25pq-s+(_zx2!2$+i+^0$kft0&y3kwmj7j5a#d_jop)$d061"
|
# generate with openssl rand -base64 64
|
||||||
|
SECRET_KEY = get_env(
|
||||||
|
"SECRET_KEY", "django-insecure-$b25pq-s+(_zx2!2$+i+^0$kft0&y3kwmj7j5a#d_jop)$d061"
|
||||||
|
)
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = get_env("DEBUG", False)
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"yt2podcast.apps.Yt2PodcastConfig",
|
"ucast.apps.UcastConfig",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"bulma",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -48,7 +98,7 @@ MIDDLEWARE = [
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "ucast.urls"
|
ROOT_URLCONF = "ucast_project.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
@ -66,16 +116,40 @@ TEMPLATES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "ucast.wsgi.application"
|
WSGI_APPLICATION = "ucast_project.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_db_config() -> dict:
|
||||||
|
db_name = get_env("DB_NAME", "db")
|
||||||
|
db_engine = get_env("DB_ENGINE", "sqlite")
|
||||||
|
|
||||||
|
if db_engine == "sqlite":
|
||||||
|
return {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / f"{db_name}.sqlite",
|
||||||
|
}
|
||||||
|
|
||||||
|
db_port = get_env("DB_PORT")
|
||||||
|
if not db_port:
|
||||||
|
if db_engine == "postgresql":
|
||||||
|
db_port = "5432"
|
||||||
|
elif db_engine == "mysql":
|
||||||
|
db_port = "3306"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ENGINE": f"django.db.backends.{db_engine}",
|
||||||
|
"NAME": db_name,
|
||||||
|
"USER": get_env("DB_USER"),
|
||||||
|
"PASSWORD": get_env("DB_PASSWORD"),
|
||||||
|
"HOST": get_env("DB_HOST", "127.0.0.1"),
|
||||||
|
"PORT": db_port,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": _get_db_config(),
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
|
@ -103,17 +177,18 @@ LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = False
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = False
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATIC_ROOT = BASE_DIR / "static"
|
STATIC_ROOT = get_env_path("STATIC_ROOT", BASE_DIR / "static")
|
||||||
|
DOWNLOAD_ROOT = get_env_path("DOWNLOAD_ROOT", BASE_DIR / "data")
|
||||||
|
|
||||||
DATA_ROOT = BASE_DIR / "data"
|
STATICFILES_DIRS = [resources.path("ucast", "static")]
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
|
@ -1 +0,0 @@
|
||||||
# coding=utf-8
|
|
|
@ -1,88 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from operator import itemgetter
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from scrapetube import scrapetube
|
|
||||||
from yt_dlp import YoutubeDL
|
|
||||||
|
|
||||||
from yt2podcast.service import util
|
|
||||||
|
|
||||||
|
|
||||||
def get_thumbnail_url(vinfo):
|
|
||||||
"""Get the best quality thumbnail"""
|
|
||||||
return max(vinfo["thumbnails"], key=itemgetter("preference"))["url"]
|
|
||||||
|
|
||||||
|
|
||||||
def download_thumbnail(vinfo, download_path):
|
|
||||||
best_url = get_thumbnail_url(vinfo)
|
|
||||||
|
|
||||||
try:
|
|
||||||
util.download_file(best_url, download_path)
|
|
||||||
except requests.exceptions.HTTPError:
|
|
||||||
default_url = vinfo["thumbnail"]
|
|
||||||
util.download_file(default_url, download_path)
|
|
||||||
|
|
||||||
|
|
||||||
def get_video_info(video_id):
|
|
||||||
with YoutubeDL() as ydl:
|
|
||||||
return ydl.extract_info(video_id, download=False)
|
|
||||||
|
|
||||||
|
|
||||||
def download_video(video_id, download_path, sponsorblock=False):
|
|
||||||
ydl_params = {
|
|
||||||
"format": "bestaudio",
|
|
||||||
"postprocessors": [
|
|
||||||
{"key": "FFmpegExtractAudio", "preferredcodec": "mp3"},
|
|
||||||
],
|
|
||||||
"outtmpl": download_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
if sponsorblock:
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
ydl_params["postprocessors"].extend(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "SponsorBlock",
|
|
||||||
"categories": ["sponsor"],
|
|
||||||
"when": "after_filter",
|
|
||||||
},
|
|
||||||
{"key": "ModifyChapters", "remove_sponsor_segments": ["sponsor"]},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
with YoutubeDL(ydl_params) as ydl:
|
|
||||||
# extract_info downloads the video and returns its metadata
|
|
||||||
return ydl.extract_info(video_id)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ChannelMetadata:
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
avatar_url: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_channel_metadata(channel_url):
|
|
||||||
session = requests.Session()
|
|
||||||
session.headers[
|
|
||||||
"User-Agent"
|
|
||||||
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
|
|
||||||
(KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36"
|
|
||||||
|
|
||||||
url = f"{channel_url}/videos?view=0&flow=grid"
|
|
||||||
|
|
||||||
html = scrapetube.get_initial_data(session, url)
|
|
||||||
data = json.loads(
|
|
||||||
scrapetube.get_json_from_html(html, "var ytInitialData = ", 0, "};") + "}"
|
|
||||||
)
|
|
||||||
metadata = data["metadata"]["channelMetadataRenderer"]
|
|
||||||
|
|
||||||
channel_id = metadata["externalId"]
|
|
||||||
name = metadata["title"]
|
|
||||||
description = metadata["description"]
|
|
||||||
avatar = metadata["avatar"]["thumbnails"][0]["url"]
|
|
||||||
|
|
||||||
return ChannelMetadata(channel_id, name, description, avatar)
|
|
|
@ -1,4 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
from importlib.resources import files
|
|
||||||
|
|
||||||
DIR_TESTFILES = files("yt2podcast.tests.testfiles")
|
|
Before Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 245 KiB |
Before Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 216 KiB |
|
@ -1,5 +0,0 @@
|
||||||
### Quellen der Thumbnails/Avatarbilder zum Testen
|
|
||||||
|
|
||||||
- a1/t1: [ThetaDev](https://www.youtube.com/channel/UCGiJh0NZ52wRhYKYnuZI08Q) (CC-BY)
|
|
||||||
- a2/t2: [Blender](https://www.youtube.com/c/BlenderFoundation) (CC-BY)
|
|
||||||
- a3/t3: [media.ccc.de](https://www.youtube.com/channel/UC2TXq_t06Hjdr2g_KdKpHQg) (CC-BY)
|
|
Before Width: | Height: | Size: 17 KiB |