diff --git a/.drone.yml b/.drone.yml index 86ea57d..155b785 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ type: docker steps: - name: Test - image: thetadev256/ucast-dev + image: d21d3q/python-poetry:3.10 commands: - poetry install - poetry run invoke lint diff --git a/.env b/.env deleted file mode 100644 index f600be1..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -UCAST_DEBUG=True -UCAST_WORKDIR=_run diff --git a/.gitignore b/.gitignore index 366f892..1ddb610 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ __pycache__ *.egg-info .pytest_cache -# JS -node_modules - # Jupyter .ipynb_checkpoints @@ -21,7 +18,5 @@ node_modules # Application data /_run +.env *.sqlite3 - -# Generated assets -/ucast/static/bulma/css diff --git a/README.md b/README.md index 22db521..329bbb3 100644 --- a/README.md +++ b/README.md @@ -24,30 +24,5 @@ Für ein ansehnliches Ansehen sorgt Bootstrap. ### Project aufsetzen -1. Python3 + Node.js + [Poetry](https://python-poetry.org/) dependency manager + - [pnpm](https://pnpm.io/) installieren -2. Python-Dependencies mit ``poetry install`` installieren -3. Node-Dependencies mit ``pnpm i`` installerien - -### Tasks (Python) - -Ausführen: `invoke ` - -`test` Unittests ausführen - -`lint` Codequalität/Formatierung überprüfen - -`format` Code mit black formatieren - -`makemigrations` Datenbankmigration erstellen - -`get-cover --vid ` YouTube-Thumbnail herunterladen -und Coverbilder zum Testen erzeugen (werden unter `ucast/tests/testfiles` abgelegt) - -### Tasks (Node.js) - -Ausführen: `npm run ` - -`start` Sass-Stylesheets automatisch bei Änderungen kompilieren - -`build` Sass-Stylesheets kompilieren und optimieren +1. Python3 + [Poetry](https://python-poetry.org/) dependency manager installieren +2. Dependencies mit ``poetry install`` installieren diff --git a/assets/icons/logo.svg b/assets/icons/logo.svg deleted file mode 100644 index ad05aa8..0000000 --- a/assets/icons/logo.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/assets/icons/logo_border.svg b/assets/icons/logo_border.svg deleted file mode 100644 index dfcf38f..0000000 --- a/assets/icons/logo_border.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/logo_dark.svg b/assets/icons/logo_dark.svg deleted file mode 100644 index 4e4d75e..0000000 --- a/assets/icons/logo_dark.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/assets/icons/yt_icon.svg b/assets/icons/yt_icon.svg deleted file mode 100644 index 2b82d1a..0000000 --- a/assets/icons/yt_icon.svg +++ /dev/null @@ -1,43 +0,0 @@ - - diff --git a/assets/sass/style.sass b/assets/sass/style.sass deleted file mode 100644 index a74478f..0000000 --- a/assets/sass/style.sass +++ /dev/null @@ -1,9 +0,0 @@ -// 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 \ No newline at end of file diff --git a/deploy/Devcontainer.Dockerfile b/deploy/Devcontainer.Dockerfile deleted file mode 100644 index 571102f..0000000 --- a/deploy/Devcontainer.Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# 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; diff --git a/manage.py b/manage.py index b71322b..9986394 120000 --- a/manage.py +++ b/manage.py @@ -1 +1 @@ -ucast_project/manage.py \ No newline at end of file +ucast/manage.py \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 0520d67..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 2ef8bfb..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,646 +0,0 @@ -lockfileVersion: 5.3 - -specifiers: - autoprefixer: ^10.4.7 - bulma: ^0.9.4 - clean-css-cli: ^5.6.0 - postcss: ^8.4.13 - postcss-cli: ^9.1.0 - rimraf: ^3.0.2 - sass: ^1.51.0 - -dependencies: - bulma: 0.9.4 - -devDependencies: - autoprefixer: 10.4.7_postcss@8.4.13 - clean-css-cli: 5.6.0 - postcss: 8.4.13 - postcss-cli: 9.1.0_postcss@8.4.13 - rimraf: 3.0.2 - sass: 1.51.0 - -packages: - - /@nodelib/fs.scandir/2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat/2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk/1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - dev: true - - /ansi-regex/5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles/4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /anymatch/3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - - /array-union/3.0.1: - resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} - engines: {node: '>=12'} - dev: true - - /autoprefixer/10.4.7_postcss@8.4.13: - resolution: {integrity: sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - dependencies: - browserslist: 4.20.3 - caniuse-lite: 1.0.30001339 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.13 - postcss-value-parser: 4.2.0 - dev: true - - /balanced-match/1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /binary-extensions/2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /brace-expansion/1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces/3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /browserslist/4.20.3: - resolution: {integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001339 - electron-to-chromium: 1.4.137 - escalade: 3.1.1 - node-releases: 2.0.4 - picocolors: 1.0.0 - dev: true - - /bulma/0.9.4: - resolution: {integrity: sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==} - dev: false - - /caniuse-lite/1.0.30001339: - resolution: {integrity: sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==} - dev: true - - /chokidar/3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /clean-css-cli/5.6.0: - resolution: {integrity: sha512-68vorNEG808D1QzeerO9AlwQVTuaR8YSK4aqwIsjJq0wDSyPH11ApHY0O+EQrdEGUZcN+d72v+Nn/gpxjAFewQ==} - engines: {node: '>= 10.12.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - clean-css: 5.3.0 - commander: 7.2.0 - glob: 7.2.0 - dev: true - - /clean-css/5.3.0: - resolution: {integrity: sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==} - engines: {node: '>= 10.0'} - dependencies: - source-map: 0.6.1 - dev: true - - /cliui/7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /color-convert/2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name/1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /commander/7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - dev: true - - /concat-map/0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} - dev: true - - /dependency-graph/0.11.0: - resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} - engines: {node: '>= 0.6.0'} - dev: true - - /dir-glob/3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /electron-to-chromium/1.4.137: - resolution: {integrity: sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==} - dev: true - - /emoji-regex/8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /escalade/3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /fast-glob/3.2.11: - resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fastq/1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} - dependencies: - reusify: 1.0.4 - dev: true - - /fill-range/7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /fraction.js/4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - dev: true - - /fs-extra/10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - dependencies: - graceful-fs: 4.2.10 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: true - - /fs.realpath/1.0.0: - resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} - dev: true - - /fsevents/2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /get-caller-file/2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true - - /get-stdin/9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} - dev: true - - /glob-parent/5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob/7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globby/12.2.0: - resolution: {integrity: sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - array-union: 3.0.1 - dir-glob: 3.0.1 - fast-glob: 3.2.11 - ignore: 5.2.0 - merge2: 1.4.1 - slash: 4.0.0 - dev: true - - /graceful-fs/4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true - - /ignore/5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} - engines: {node: '>= 4'} - dev: true - - /immutable/4.0.0: - resolution: {integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==} - dev: true - - /inflight/1.0.6: - resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits/2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /is-binary-path/2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - - /is-extglob/2.1.1: - resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} - engines: {node: '>=0.10.0'} - dev: true - - /is-fullwidth-code-point/3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /is-glob/4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-number/7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /jsonfile/6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.10 - dev: true - - /lilconfig/2.0.5: - resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} - engines: {node: '>=10'} - dev: true - - /merge2/1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch/4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - - /minimatch/3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /nanoid/3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /node-releases/2.0.4: - resolution: {integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==} - dev: true - - /normalize-path/3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /normalize-range/0.1.2: - resolution: {integrity: sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=} - engines: {node: '>=0.10.0'} - dev: true - - /once/1.4.0: - resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} - dependencies: - wrappy: 1.0.2 - dev: true - - /path-is-absolute/1.0.1: - resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} - engines: {node: '>=0.10.0'} - dev: true - - /path-type/4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /picocolors/1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true - - /picomatch/2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /pify/2.3.0: - resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} - engines: {node: '>=0.10.0'} - dev: true - - /postcss-cli/9.1.0_postcss@8.4.13: - resolution: {integrity: sha512-zvDN2ADbWfza42sAnj+O2uUWyL0eRL1V+6giM2vi4SqTR3gTYy8XzcpfwccayF2szcUif0HMmXiEaDv9iEhcpw==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - postcss: ^8.0.0 - dependencies: - chokidar: 3.5.3 - dependency-graph: 0.11.0 - fs-extra: 10.1.0 - get-stdin: 9.0.0 - globby: 12.2.0 - picocolors: 1.0.0 - postcss: 8.4.13 - postcss-load-config: 3.1.4_postcss@8.4.13 - postcss-reporter: 7.0.5_postcss@8.4.13 - pretty-hrtime: 1.0.3 - read-cache: 1.0.0 - slash: 4.0.0 - yargs: 17.4.1 - transitivePeerDependencies: - - ts-node - dev: true - - /postcss-load-config/3.1.4_postcss@8.4.13: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 2.0.5 - postcss: 8.4.13 - yaml: 1.10.2 - dev: true - - /postcss-reporter/7.0.5_postcss@8.4.13: - resolution: {integrity: sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==} - engines: {node: '>=10'} - peerDependencies: - postcss: ^8.1.0 - dependencies: - picocolors: 1.0.0 - postcss: 8.4.13 - thenby: 1.3.4 - dev: true - - /postcss-value-parser/4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true - - /postcss/8.4.13: - resolution: {integrity: sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true - - /pretty-hrtime/1.0.3: - resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=} - engines: {node: '>= 0.8'} - dev: true - - /queue-microtask/1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /read-cache/1.0.0: - resolution: {integrity: sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=} - dependencies: - pify: 2.3.0 - dev: true - - /readdirp/3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /require-directory/2.1.1: - resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} - engines: {node: '>=0.10.0'} - dev: true - - /reusify/1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf/3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.0 - dev: true - - /run-parallel/1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /sass/1.51.0: - resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - immutable: 4.0.0 - source-map-js: 1.0.2 - dev: true - - /slash/4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - dev: true - - /source-map-js/1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: true - - /source-map/0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - - /string-width/4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - - /strip-ansi/6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /thenby/1.3.4: - resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} - dev: true - - /to-regex-range/5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /universalify/2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} - dev: true - - /wrap-ansi/7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /wrappy/1.0.2: - resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} - dev: true - - /y18n/5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - - /yaml/1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: true - - /yargs-parser/21.0.1: - resolution: {integrity: sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==} - engines: {node: '>=12'} - dev: true - - /yargs/17.4.1: - resolution: {integrity: sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==} - engines: {node: '>=12'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.0.1 - dev: true diff --git a/poetry.lock b/poetry.lock index eeea053..195374a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -157,17 +157,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] 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]] name = "feedparser" version = "6.0.8" @@ -250,14 +239,6 @@ category = "main" optional = false 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]] name = "nodeenv" version = "1.6.0" @@ -329,14 +310,6 @@ pyyaml = ">=5.1" toml = "*" 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]] name = "py" version = "1.11.0" @@ -423,17 +396,6 @@ pytest = ">=5.4.0" docs = ["sphinx", "sphinx-rtd-theme"] 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]] name = "pyyaml" version = "6.0" @@ -602,7 +564,7 @@ websockets = "*" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "8609785f53a44a16f3c5c1d5042ab2627bb198f3c7daa8ea18e55bf1e66c4345" +content-hash = "99e2a5970962f1e936da2010b8ec997026f5afe4762af4345da287125e6b7771" [metadata.files] asgiref = [ @@ -840,10 +802,6 @@ django = [ {file = "Django-4.0.4-py3-none-any.whl", hash = "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687"}, {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 = [ {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, @@ -882,13 +840,6 @@ mutagen = [ {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, {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 = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, @@ -949,19 +900,6 @@ pre-commit = [ {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"}, ] -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 = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1015,10 +953,6 @@ pytest-django = [ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, {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 = [ {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"}, diff --git a/pyproject.toml b/pyproject.toml index d783024..0323b68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "YouTube to Podcast converter" authors = ["Theta-Dev "] packages = [ { include = "ucast" }, - { include = "ucast_project" }, + { include = "yt2podcast" }, ] [tool.poetry.dependencies] @@ -21,10 +21,6 @@ wcag-contrast-ratio = "^0.9" font-source-sans-pro = "^0.0.1" fonts = "^0.0.3" 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] pytest = "^7.1.1" @@ -34,7 +30,7 @@ pytest-django = "^4.5.2" pre-commit = "^2.19.0" [tool.poetry.scripts] -"ucast-manage" = "ucast_project.manage:main" +"ucast-manage" = "ucast.manage:main" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tasks.py b/tasks.py index 3c9a2df..832c200 100644 --- a/tasks.py +++ b/tasks.py @@ -1,75 +1,40 @@ import os from pathlib import Path -from invoke import Responder, task +from invoke import task -from ucast import tests -from ucast.service import cover, util, youtube +from yt2podcast import tests +from yt2podcast.service import cover, util, youtube os.chdir(Path(__file__).absolute().parent) @task def test(c): - """Run unit tests""" c.run("pytest", pty=True) @task def lint(c): - """Check for code quality and formatting""" c.run("pre-commit run -a", pty=True) @task def format(c): - """Format the code with black""" c.run("pre-commit run black -a", pty=True) @task def makemigrations(c): - """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], - ) + c.run("python manage.py makemigrations yt2podcast") @task 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) - title = vinfo.title - channel_name = vinfo.channel_name - channel_url = vinfo.channel_url + title = vinfo["fulltitle"] + channel_name = vinfo["uploader"] + channel_url = vinfo["channel_url"] channel_metadata = youtube.get_channel_metadata(channel_url) ti = 1 @@ -90,11 +55,3 @@ def get_cover(c, vid=""): cover.create_cover_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" - ) diff --git a/ucast/__init__.py b/ucast/__init__.py index e69de29..9bad579 100644 --- a/ucast/__init__.py +++ b/ucast/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/ucast_project/asgi.py b/ucast/asgi.py similarity index 81% rename from ucast_project/asgi.py rename to ucast/asgi.py index 23dea34..be266bd 100644 --- a/ucast_project/asgi.py +++ b/ucast/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings") application = get_asgi_application() diff --git a/ucast_project/manage.py b/ucast/manage.py similarity index 88% rename from ucast_project/manage.py rename to ucast/manage.py index f4ede1d..4c6dfe3 100755 --- a/ucast_project/manage.py +++ b/ucast/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/ucast/resources/yt_icon.png b/ucast/resources/yt_icon.png deleted file mode 100644 index 5d2fac6..0000000 Binary files a/ucast/resources/yt_icon.png and /dev/null differ diff --git a/ucast/service/youtube.py b/ucast/service/youtube.py deleted file mode 100644 index b02a3cd..0000000 --- a/ucast/service/youtube.py +++ /dev/null @@ -1,145 +0,0 @@ -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) diff --git a/ucast_project/settings.py b/ucast/settings.py similarity index 52% rename from ucast_project/settings.py rename to ucast/settings.py index 018668d..ffcdcd0 100644 --- a/ucast_project/settings.py +++ b/ucast/settings.py @@ -10,82 +10,32 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ -import os -from importlib import resources from pathlib import Path -import dotenv - -VAR_PREFIX = "UCAST_" - - -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() +BASE_DIR = Path(__file__).resolve().parent.parent / "_run" + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -# 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" -) +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! -DEBUG = get_env("DEBUG", False) +DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - "ucast.apps.UcastConfig", + "yt2podcast.apps.Yt2PodcastConfig", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "bulma", ] MIDDLEWARE = [ @@ -98,7 +48,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "ucast_project.urls" +ROOT_URLCONF = "ucast.urls" TEMPLATES = [ { @@ -116,40 +66,16 @@ TEMPLATES = [ }, ] -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, - } - +WSGI_APPLICATION = "ucast.wsgi.application" # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases + DATABASES = { - "default": _get_db_config(), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } } # Password validation @@ -177,18 +103,17 @@ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" -USE_I18N = False +USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ STATIC_URL = "static/" -STATIC_ROOT = get_env_path("STATIC_ROOT", BASE_DIR / "static") -DOWNLOAD_ROOT = get_env_path("DOWNLOAD_ROOT", BASE_DIR / "data") +STATIC_ROOT = BASE_DIR / "static" -STATICFILES_DIRS = [resources.path("ucast", "static")] +DATA_ROOT = BASE_DIR / "data" # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field diff --git a/ucast/tests/__init__.py b/ucast/tests/__init__.py deleted file mode 100644 index 293ff6c..0000000 --- a/ucast/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from importlib.resources import files - -DIR_TESTFILES = files("ucast.tests.testfiles") diff --git a/ucast/tests/test_util.py b/ucast/tests/test_util.py deleted file mode 100644 index 895b78d..0000000 --- a/ucast/tests/test_util.py +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/ucast/tests/test_youtube.py b/ucast/tests/test_youtube.py deleted file mode 100644 index 151a3e6..0000000 --- a/ucast/tests/test_youtube.py +++ /dev/null @@ -1,129 +0,0 @@ -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 diff --git a/ucast/tests/testfiles/cover/blank.png b/ucast/tests/testfiles/cover/blank.png deleted file mode 100644 index 6e79a4d..0000000 Binary files a/ucast/tests/testfiles/cover/blank.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c1_blur.png b/ucast/tests/testfiles/cover/c1_blur.png deleted file mode 100644 index 8b0bd15..0000000 Binary files a/ucast/tests/testfiles/cover/c1_blur.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c1_noavatar.png b/ucast/tests/testfiles/cover/c1_noavatar.png deleted file mode 100644 index d708050..0000000 Binary files a/ucast/tests/testfiles/cover/c1_noavatar.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c2_blur.png b/ucast/tests/testfiles/cover/c2_blur.png deleted file mode 100644 index 152d2b5..0000000 Binary files a/ucast/tests/testfiles/cover/c2_blur.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c2_classic.png b/ucast/tests/testfiles/cover/c2_classic.png deleted file mode 100644 index 7c38fff..0000000 Binary files a/ucast/tests/testfiles/cover/c2_classic.png and /dev/null differ diff --git a/ucast/tests/testfiles/cover/c3_blur.png b/ucast/tests/testfiles/cover/c3_blur.png deleted file mode 100644 index 3b919de..0000000 Binary files a/ucast/tests/testfiles/cover/c3_blur.png and /dev/null differ diff --git a/ucast/tests/testfiles/sources.md b/ucast/tests/testfiles/sources.md deleted file mode 100644 index 92f6d05..0000000 --- a/ucast/tests/testfiles/sources.md +++ /dev/null @@ -1,10 +0,0 @@ -### 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) \ No newline at end of file diff --git a/ucast/tests/testfiles/thumbnail/t2.webp b/ucast/tests/testfiles/thumbnail/t2.webp deleted file mode 100644 index 0ed7f2b..0000000 Binary files a/ucast/tests/testfiles/thumbnail/t2.webp and /dev/null differ diff --git a/ucast_project/urls.py b/ucast/urls.py similarity index 100% rename from ucast_project/urls.py rename to ucast/urls.py diff --git a/ucast_project/wsgi.py b/ucast/wsgi.py similarity index 81% rename from ucast_project/wsgi.py rename to ucast/wsgi.py index b8762e6..06375f4 100644 --- a/ucast_project/wsgi.py +++ b/ucast/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast_project.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucast.settings") application = get_wsgi_application() diff --git a/ucast_project/__init__.py b/ucast_project/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ucast/migrations/__init__.py b/yt2podcast/__init__.py similarity index 100% rename from ucast/migrations/__init__.py rename to yt2podcast/__init__.py diff --git a/ucast/admin.py b/yt2podcast/admin.py similarity index 100% rename from ucast/admin.py rename to yt2podcast/admin.py diff --git a/ucast/apps.py b/yt2podcast/apps.py similarity index 61% rename from ucast/apps.py rename to yt2podcast/apps.py index ad4b110..563d9a1 100644 --- a/ucast/apps.py +++ b/yt2podcast/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class UcastConfig(AppConfig): +class Yt2PodcastConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "ucast" + name = "yt2podcast" diff --git a/ucast/migrations/0001_initial.py b/yt2podcast/migrations/0001_initial.py similarity index 95% rename from ucast/migrations/0001_initial.py rename to yt2podcast/migrations/0001_initial.py index d9d9bd5..fed1734 100644 --- a/ucast/migrations/0001_initial.py +++ b/yt2podcast/migrations/0001_initial.py @@ -34,7 +34,7 @@ class Migration(migrations.Migration): ("title", models.CharField(max_length=200)), ("slug", models.CharField(max_length=209)), ("published", models.DateTimeField()), - ("downloaded", models.DateTimeField(null=True)), + ("downloaded", models.DateTimeField()), ("description", models.TextField()), ], ), diff --git a/ucast/service/__init__.py b/yt2podcast/migrations/__init__.py similarity index 100% rename from ucast/service/__init__.py rename to yt2podcast/migrations/__init__.py diff --git a/ucast/models.py b/yt2podcast/models.py similarity index 92% rename from ucast/models.py rename to yt2podcast/models.py index 7f6711e..486abad 100644 --- a/ucast/models.py +++ b/yt2podcast/models.py @@ -15,5 +15,5 @@ class Video(models.Model): title = models.CharField(max_length=200) slug = models.CharField(max_length=209) published = models.DateTimeField() - downloaded = models.DateTimeField(null=True) + downloaded = models.DateTimeField() description = models.TextField() diff --git a/yt2podcast/service/__init__.py b/yt2podcast/service/__init__.py new file mode 100644 index 0000000..9bad579 --- /dev/null +++ b/yt2podcast/service/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/ucast/service/cover.py b/yt2podcast/service/cover.py similarity index 56% rename from ucast/service/cover.py rename to yt2podcast/service/cover.py index 5e6444d..8049b79 100644 --- a/ucast/service/cover.py +++ b/yt2podcast/service/cover.py @@ -1,6 +1,6 @@ +# coding=utf-8 import enum import math -from importlib import resources from pathlib import Path from typing import List, Optional, Tuple @@ -10,7 +10,7 @@ from colorthief import ColorThief from fonts.ttf import SourceSansPro from PIL import Image, ImageDraw, ImageFilter, ImageFont -from ucast.service import typ +from yt2podcast.service import typ CHAR_ELLIPSIS = "…" COVER_WIDTH = 500 @@ -24,17 +24,6 @@ class CoverStyle(enum.Enum): def _split_text( height: int, width: int, text: str, font: ImageFont.FreeTypeFont, line_spacing=0 ) -> 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: return [] @@ -91,19 +80,6 @@ def _draw_text_box( line_spacing=0, 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 height = y_br - y_tl width = x_br - x_tl @@ -120,41 +96,19 @@ def _draw_text_box( draw.text((x_tl, y_pos), line, color, font) -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 - """ +def _get_dominant_color(img: Image.Image): thief = ColorThief.__new__(ColorThief) thief.image = img return thief.get_color() -def _interpolate_color(color_from: typ.Color, color_to: typ.Color, steps: int): - """ - 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): +def _interpolate_color(color_from: typ.Color, color_to: typ.Color, interval: int): + det_co = [(t - f) / interval for f, t in zip(color_from, color_to)] + for i in range(interval): yield [round(f + det * i) for f, det in zip(color_from, det_co)] 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]) c_blk = wcag_contrast_ratio.rgb((0, 0, 0), color_decimal) c_wht = wcag_contrast_ratio.rgb((1, 1, 1), color_decimal) @@ -169,26 +123,17 @@ def _get_baseimage( bottom_color: typ.Color, 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: ctn_width = int(COVER_WIDTH / thumbnail.height * thumbnail.width) - ctn_x_left = int((ctn_width - COVER_WIDTH) / 2) - - ctn = thumbnail.resize( - (ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS - ).filter(ImageFilter.GaussianBlur(20)) - cover.paste(ctn, (-ctn_x_left, 0)) + ctn_l = int((ctn_width - COVER_WIDTH) / 2) + ctn_r = ctn_width - ctn_l + cover = ( + thumbnail.resize((ctn_width, COVER_WIDTH), Image.Resampling.LANCZOS) + .crop((ctn_l, 0, ctn_r, COVER_WIDTH)) + .filter(ImageFilter.GaussianBlur(20)) + ) else: + cover = Image.new("RGB", (COVER_WIDTH, COVER_WIDTH)) cover_draw = ImageDraw.Draw(cover) # Draw background gradient @@ -200,13 +145,14 @@ def _get_baseimage( return cover -def _resize_thumbnail(thumbnail: Image.Image) -> Image.Image: - """ - Scale thumbnail image down to cover size and remove black bars - - :param thumbnail: Thumbnail image object - :return: Resized thumbnail image object - """ +def _create_cover_image( + thumbnail: Image.Image, + avatar: Optional[Image.Image], + title: str, + channel: str, + style: CoverStyle, +) -> Image.Image: + # Remove black bars from thumbnail thumbnail = bordercrop.crop( thumbnail, MINIMUM_ROWS=int(thumbnail.height * 0.1), @@ -217,31 +163,30 @@ def _resize_thumbnail(thumbnail: Image.Image) -> Image.Image: tn_resize_height = int(COVER_WIDTH / thumbnail.width * thumbnail.height) tn_16_9_height = int(COVER_WIDTH / 16 * 9) tn_height = min(tn_resize_height, tn_16_9_height) - tn_crop_y_top = int((tn_resize_height - tn_height) / 2) - tn_crop_y_bottom = tn_resize_height - tn_crop_y_top - - return thumbnail.resize( + tn_crop_t = int((tn_resize_height - tn_height) / 2) + tn_crop_b = tn_resize_height - tn_crop_t + tn = thumbnail.resize( (COVER_WIDTH, tn_resize_height), Image.Resampling.LANCZOS - ).crop((0, tn_crop_y_top, COVER_WIDTH, tn_crop_y_bottom)) + ).crop((0, tn_crop_t, COVER_WIDTH, tn_crop_b)) + # 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) -def _draw_text_avatar( - cover: Image.Image, - avatar: Optional[Image.Image], - title: str, - channel: str, - top_color: typ.Color, - bottom_color: typ.Color, -): + cover = _get_baseimage(thumbnail, top_color, bottom_color, style) 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 avt_margin = 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: avt_margin = int(tn_16_9_margin * 0.05) avt_size = tn_16_9_margin - 2 * avt_margin @@ -291,90 +236,22 @@ def _draw_text_avatar( 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 def create_cover_file( - thumbnail_path: Optional[Path], + thumbnail_path: Path, avatar_path: Optional[Path], title: str, channel: str, style: CoverStyle, cover_path: 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) + thumbnail = Image.open(thumbnail_path) avatar = None if avatar_path: avatar = Image.open(avatar_path) - if thumbnail: - cvr = _create_cover_image(thumbnail, avatar, title, channel, style) - else: - cvr = _create_blank_cover_image(avatar, title, channel) - + cvr = _create_cover_image(thumbnail, avatar, title, channel, style) cvr.save(cover_path) diff --git a/ucast/service/typ.py b/yt2podcast/service/typ.py similarity index 78% rename from ucast/service/typ.py rename to yt2podcast/service/typ.py index 75efbf1..dbf0d5a 100644 --- a/ucast/service/typ.py +++ b/yt2podcast/service/typ.py @@ -1,3 +1,4 @@ +# coding=utf-8 from typing import Tuple Color = Tuple[int, int, int] diff --git a/ucast/service/util.py b/yt2podcast/service/util.py similarity index 92% rename from ucast/service/util.py rename to yt2podcast/service/util.py index 011bedd..c58a39c 100644 --- a/ucast/service/util.py +++ b/yt2podcast/service/util.py @@ -1,3 +1,4 @@ +# coding=utf-8 import requests diff --git a/yt2podcast/service/youtube.py b/yt2podcast/service/youtube.py new file mode 100644 index 0000000..a880ef2 --- /dev/null +++ b/yt2podcast/service/youtube.py @@ -0,0 +1,88 @@ +# 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) diff --git a/yt2podcast/tests/__init__.py b/yt2podcast/tests/__init__.py new file mode 100644 index 0000000..b383d20 --- /dev/null +++ b/yt2podcast/tests/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 +from importlib.resources import files + +DIR_TESTFILES = files("yt2podcast.tests.testfiles") diff --git a/ucast/tests/test_cover.py b/yt2podcast/tests/test_cover.py similarity index 74% rename from ucast/tests/test_cover.py rename to yt2podcast/tests/test_cover.py index ca50256..8a4d457 100644 --- a/ucast/tests/test_cover.py +++ b/yt2podcast/tests/test_cover.py @@ -1,3 +1,4 @@ +# coding=utf-8 import tempfile from pathlib import Path from typing import List @@ -6,8 +7,8 @@ import pytest from fonts.ttf import SourceSansPro from PIL import Image, ImageChops, ImageFont -from ucast import tests -from ucast.service import cover, typ +from yt2podcast import tests +from yt2podcast.service import cover, typ @pytest.mark.parametrize( @@ -49,7 +50,7 @@ def test_split_text(height: int, width: int, text: str, expect: List[str]): "file_name,color", [ ("t1.webp", (63, 63, 62)), - ("t2.webp", (22, 20, 20)), + ("t2.webp", (17, 14, 15)), ("t3.webp", (54, 24, 28)), ], ) @@ -117,47 +118,6 @@ def test_create_cover_image( 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) assert diff.getbbox() is None diff --git a/ucast/tests/testfiles/avatar/a1.jpg b/yt2podcast/tests/testfiles/avatar/a1.jpg similarity index 100% rename from ucast/tests/testfiles/avatar/a1.jpg rename to yt2podcast/tests/testfiles/avatar/a1.jpg diff --git a/ucast/tests/testfiles/avatar/a2.jpg b/yt2podcast/tests/testfiles/avatar/a2.jpg similarity index 100% rename from ucast/tests/testfiles/avatar/a2.jpg rename to yt2podcast/tests/testfiles/avatar/a2.jpg diff --git a/ucast/tests/testfiles/avatar/a3.jpg b/yt2podcast/tests/testfiles/avatar/a3.jpg similarity index 100% rename from ucast/tests/testfiles/avatar/a3.jpg rename to yt2podcast/tests/testfiles/avatar/a3.jpg diff --git a/yt2podcast/tests/testfiles/cover/c1_blur.png b/yt2podcast/tests/testfiles/cover/c1_blur.png new file mode 100644 index 0000000..1ea1caf Binary files /dev/null and b/yt2podcast/tests/testfiles/cover/c1_blur.png differ diff --git a/ucast/tests/testfiles/cover/c1_classic.png b/yt2podcast/tests/testfiles/cover/c1_classic.png similarity index 100% rename from ucast/tests/testfiles/cover/c1_classic.png rename to yt2podcast/tests/testfiles/cover/c1_classic.png diff --git a/yt2podcast/tests/testfiles/cover/c2_blur.png b/yt2podcast/tests/testfiles/cover/c2_blur.png new file mode 100644 index 0000000..453d455 Binary files /dev/null and b/yt2podcast/tests/testfiles/cover/c2_blur.png differ diff --git a/yt2podcast/tests/testfiles/cover/c2_classic.png b/yt2podcast/tests/testfiles/cover/c2_classic.png new file mode 100644 index 0000000..3fb1cae Binary files /dev/null and b/yt2podcast/tests/testfiles/cover/c2_classic.png differ diff --git a/yt2podcast/tests/testfiles/cover/c3_blur.png b/yt2podcast/tests/testfiles/cover/c3_blur.png new file mode 100644 index 0000000..24b24bd Binary files /dev/null and b/yt2podcast/tests/testfiles/cover/c3_blur.png differ diff --git a/ucast/tests/testfiles/cover/c3_classic.png b/yt2podcast/tests/testfiles/cover/c3_classic.png similarity index 100% rename from ucast/tests/testfiles/cover/c3_classic.png rename to yt2podcast/tests/testfiles/cover/c3_classic.png diff --git a/yt2podcast/tests/testfiles/sources.md b/yt2podcast/tests/testfiles/sources.md new file mode 100644 index 0000000..2456116 --- /dev/null +++ b/yt2podcast/tests/testfiles/sources.md @@ -0,0 +1,5 @@ +### 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) diff --git a/ucast/tests/testfiles/thumbnail/t1.webp b/yt2podcast/tests/testfiles/thumbnail/t1.webp similarity index 100% rename from ucast/tests/testfiles/thumbnail/t1.webp rename to yt2podcast/tests/testfiles/thumbnail/t1.webp diff --git a/yt2podcast/tests/testfiles/thumbnail/t2.webp b/yt2podcast/tests/testfiles/thumbnail/t2.webp new file mode 100644 index 0000000..b54d902 Binary files /dev/null and b/yt2podcast/tests/testfiles/thumbnail/t2.webp differ diff --git a/ucast/tests/testfiles/thumbnail/t3.webp b/yt2podcast/tests/testfiles/thumbnail/t3.webp similarity index 100% rename from ucast/tests/testfiles/thumbnail/t3.webp rename to yt2podcast/tests/testfiles/thumbnail/t3.webp diff --git a/ucast/views.py b/yt2podcast/views.py similarity index 100% rename from ucast/views.py rename to yt2podcast/views.py