├── frontend-angular ├── src │ ├── assets │ │ └── .gitkeep │ ├── _variables.scss │ ├── favicon.ico │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── typings.d.ts │ ├── tsconfig.app.json │ ├── app │ │ ├── podcasts │ │ │ ├── podcasts.component.html │ │ │ ├── podcasts.component.scss │ │ │ ├── podcasts.actions.ts │ │ │ ├── podcasts.reducer.ts │ │ │ ├── podcasts.effects.ts │ │ │ ├── core │ │ │ │ └── resolver │ │ │ │ │ └── podcasts.resolver.ts │ │ │ └── podcasts.component.ts │ │ ├── app.component.scss │ │ ├── shared │ │ │ ├── toolbar │ │ │ │ ├── toolbar.component.html │ │ │ │ ├── toolbar.component.scss │ │ │ │ ├── toolbar.module.ts │ │ │ │ ├── toolbar.component.ts │ │ │ │ └── toolbar.component.spec.ts │ │ │ ├── shared.module.ts │ │ │ └── service │ │ │ │ └── podcast │ │ │ │ └── podcast.service.ts │ │ ├── podcast │ │ │ ├── podcast.component.scss │ │ │ ├── core │ │ │ │ ├── episodes │ │ │ │ │ ├── episodes.component.html │ │ │ │ │ └── episodes.component.scss │ │ │ │ ├── podcast.resolver.ts │ │ │ │ └── podcast-items.resolver.ts │ │ │ └── podcast.reducer.ts │ │ ├── floating-player │ │ │ ├── floating-player.actions.ts │ │ │ ├── floating-player.module.ts │ │ │ ├── floating-player.component.scss │ │ │ ├── floating-player.component.html │ │ │ └── floating-player.reducer.ts │ │ ├── search │ │ │ ├── search.actions.ts │ │ │ ├── resolver │ │ │ │ ├── search-query.resolver.ts │ │ │ │ ├── search.resolver.ts │ │ │ │ └── search-query.resolver.spec.ts │ │ │ └── search.component.scss │ │ ├── item │ │ │ ├── item.component.spec.ts │ │ │ ├── core │ │ │ │ ├── item.resolver.ts │ │ │ │ └── podcast.resolver.ts │ │ │ └── item.reducer.ts │ │ ├── app.component.ts │ │ ├── app.actions.ts │ │ ├── app.component.html │ │ └── app.reducer.ts │ ├── index.html │ ├── main.ts │ ├── tsconfig.spec.json │ └── styles.scss ├── .prettierignore ├── proxy.conf.json ├── e2e │ ├── tsconfig.e2e.json │ ├── app.po.ts │ └── app.e2e-spec.ts ├── proxy-prod.conf.json ├── .editorconfig ├── prettier.config.js ├── tsconfig.json ├── protractor.conf.js ├── build.gradle.kts ├── jest.config.js └── README.md ├── frontend-angularjs ├── .jshintrc ├── .envrc ├── www │ └── app │ │ ├── common │ │ ├── modules │ │ │ ├── angularNotification.js │ │ │ ├── truncate.js │ │ │ ├── ngTagsInput.js │ │ │ └── highCharts.js │ │ ├── component │ │ │ ├── updating │ │ │ │ ├── updating.html │ │ │ │ └── updating.less │ │ │ ├── authorize-notification │ │ │ │ ├── authorize-notification.html │ │ │ │ └── authorize-notification.js │ │ │ ├── videogular │ │ │ │ ├── vg-copy │ │ │ │ │ ├── vg-copy.less │ │ │ │ │ └── vg-copy.js │ │ │ │ ├── vg-link │ │ │ │ │ ├── vg-link.less │ │ │ │ │ └── vg-link.js │ │ │ │ ├── vg-link-vlc │ │ │ │ │ ├── vg-link-vlc.less │ │ │ │ │ └── vg-link-vlc.js │ │ │ │ └── videogular.less │ │ │ ├── item-menu │ │ │ │ └── item-menu.less │ │ │ ├── navbar │ │ │ │ ├── navbar.html │ │ │ │ ├── navbar.less │ │ │ │ └── navbar.js │ │ │ ├── title │ │ │ │ └── title.js │ │ │ ├── watchlist-chooser │ │ │ │ ├── watchlist-chooser.less │ │ │ │ └── watchlist-chooser.html │ │ │ └── copy │ │ │ │ └── copy.js │ │ ├── filter │ │ │ └── html2plainText.js │ │ └── service │ │ │ ├── device-detection.js │ │ │ ├── title.service.js │ │ │ └── data │ │ │ ├── updateService.js │ │ │ ├── typeService.js │ │ │ └── tagService.js │ │ ├── config │ │ ├── ionicons │ │ │ ├── ionicons.js │ │ │ └── ionicons.less │ │ ├── styles │ │ │ ├── styles.js │ │ │ └── bootstrap-adaptation.less │ │ ├── font-awesome │ │ │ ├── font-awesome.less │ │ │ └── font-awesome.js │ │ ├── bootstrap │ │ │ ├── bootstrap.less │ │ │ └── bootstrap.js │ │ ├── loading.js │ │ ├── route.js │ │ └── config.js │ │ ├── item │ │ └── item.js │ │ ├── podcasts │ │ ├── details │ │ │ ├── upload │ │ │ │ └── upload.html │ │ │ └── stats │ │ │ │ └── stats.html │ │ └── podcasts.less │ │ ├── app.js │ │ └── stats │ │ └── stats.html ├── docker │ ├── Dockerfile │ └── default.conf ├── gulpfile.babel.js ├── gulp │ ├── utils.js │ └── tasks │ │ ├── lint.js │ │ ├── fonts.js │ │ ├── less.js │ │ ├── watch.js │ │ └── maven.js └── .eslintrc ├── gradle.properties ├── storage └── Dockerfile ├── backend └── src │ ├── test │ ├── resources │ │ ├── __files │ │ │ ├── service │ │ │ │ ├── mimeTypeService │ │ │ │ │ └── plain.text.txt │ │ │ │ ├── htmlService │ │ │ │ │ └── jsoup.html │ │ │ │ ├── jdomService │ │ │ │ │ ├── invalid.xml │ │ │ │ │ └── valid.xml │ │ │ │ └── urlService │ │ │ │ │ └── relative.m3u8 │ │ │ ├── img │ │ │ │ └── image.png │ │ │ └── utils │ │ │ │ └── multipart │ │ │ │ └── outputfile.out │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ ├── spring.properties │ │ ├── junit-platform.properties │ │ ├── remote │ │ │ ├── downloader │ │ │ │ ├── rtmpdump │ │ │ │ │ └── rtmpdump.txt │ │ │ │ └── m3u8 │ │ │ │ │ └── m3u8file.m3u8 │ │ │ └── podcast │ │ │ │ ├── dailymotion │ │ │ │ ├── karimdebbache.ids.0.item.json │ │ │ │ ├── karimdebbache-without-description.json │ │ │ │ ├── karimdebbache.ids.1.items.json │ │ │ │ ├── karimdebbache.json │ │ │ │ └── karimdebbache.ids.10.items.json │ │ │ │ └── youtube │ │ │ │ ├── joueurdugrenier.id.json │ │ │ │ ├── joueurdugrenier.channel.with-0-item.xml │ │ │ │ └── joueurdugrenier.playlist.with-0-item.xml │ │ ├── logback-test.xml │ │ ├── application.yml │ │ └── xml │ │ │ ├── podcast-with-port-not-defined-and-http.xml │ │ │ ├── podcast-with-port-not-defined-and-https.xml │ │ │ ├── podcast-with-x-forwarded-port.xml │ │ │ └── podcast-with-lots-of-parameters.xml │ └── kotlin │ │ └── com │ │ └── github │ │ └── davinkevin │ │ └── podcastserver │ │ ├── extension │ │ ├── assertthat │ │ │ └── SoftAsserts.kt │ │ └── json │ │ │ └── JsonAssert.kt │ │ ├── utils │ │ ├── custom │ │ │ └── ffmpeg │ │ │ │ ├── CustomRunProcessFuncTest.kt │ │ │ │ └── ProcessListenerTest.kt │ │ └── MatcherExtractorTest.kt │ │ ├── IOUtils.kt │ │ ├── entity │ │ └── StatusTest.kt │ │ ├── config │ │ ├── BeanConfigScanTest.kt │ │ └── ClockConfigTest.kt │ │ ├── service │ │ ├── ffmpeg │ │ │ └── FfmpegConfigTest.kt │ │ └── properties │ │ │ └── ExternalToolsTest.kt │ │ └── update │ │ └── updaters │ │ └── TypeTest.kt │ └── main │ ├── resources │ ├── application-tools-from-homebrew.yml │ ├── META-INF │ │ └── additional-spring-configuration-metadata.json │ ├── application-local-pg.yml │ ├── application-local-minio.yml │ └── application.yml │ └── kotlin │ └── com │ └── github │ └── davinkevin │ └── podcastserver │ ├── tag │ ├── Tag.kt │ ├── TagService.kt │ └── TagConfig.kt │ ├── update │ ├── updaters │ │ ├── Type.kt │ │ ├── UpdaterSelector.kt │ │ ├── upload │ │ │ ├── UploadUpdaterConfig.kt │ │ │ └── UploadUpdater.kt │ │ ├── youtube │ │ │ └── YoutubeUtils.kt │ │ ├── rss │ │ │ └── RSSUpdaterConfig.kt │ │ ├── gulli │ │ │ └── GulliUpdaterConfig.kt │ │ ├── dailymotion │ │ │ └── DailymotionUpdaterConfig.kt │ │ └── francetv │ │ │ └── FranceTvUpdaterConfig.kt │ └── UpdateHandler.kt │ ├── extension │ ├── java │ │ ├── util │ │ │ └── Optional.kt │ │ └── net │ │ │ └── URI.kt │ ├── restclient │ │ └── RestClient.kt │ └── podcastserver │ │ └── item │ │ └── Slugable.kt │ ├── rss │ ├── Rss.kt │ └── Namespaces.kt │ ├── cover │ ├── Cover.kt │ ├── CoverHandler.kt │ ├── CoverConfig.kt │ └── CoverService.kt │ ├── download │ └── downloaders │ │ ├── rtmp │ │ └── RTMPDownloaderConfig.kt │ │ ├── ffmpeg │ │ └── FfmpegDownloaderConfig.kt │ │ ├── youtubedl │ │ ├── YTDlpParameters.kt │ │ └── YoutubeDlUtils.kt │ │ ├── Downloader.kt │ │ └── DownloaderSelector.kt │ ├── database │ ├── StatusConverter.kt │ └── PathConverter.kt │ ├── config │ ├── ClockConfig.kt │ └── TomcatConfig.kt │ ├── find │ ├── finders │ │ ├── noop │ │ │ ├── NoopConfig.kt │ │ │ └── NoOpFinder.kt │ │ ├── Finder.kt │ │ ├── FindersExtension.kt │ │ ├── francetv │ │ │ └── FranceTvFinderConfig.kt │ │ ├── rss │ │ │ └── RSSFinderConfig.kt │ │ ├── itunes │ │ │ └── ItunesFinderConfig.kt │ │ ├── dailymotion │ │ │ └── DailymotionFinderConfig.kt │ │ ├── youtube │ │ │ └── YoutubeFinderConfig.kt │ │ ├── gulli │ │ │ └── GulliFinderConfig.kt │ │ └── mytf1 │ │ │ └── MyTf1FinderConfig.kt │ ├── FindPocastInformation.kt │ ├── FindHandler.kt │ └── FindService.kt │ ├── service │ ├── image │ │ └── ImageServiceConfig.kt │ ├── properties │ │ ├── ExternalTools.kt │ │ └── PodcastServerParameters.kt │ ├── ProcessService.kt │ ├── storage │ │ ├── CoverExists.kt │ │ ├── ToExternalUrl.kt │ │ └── DeleteObject.kt │ └── ffmpeg │ │ └── FfmpegConfig.kt │ ├── kodi │ ├── Kodi.kt │ └── KodiConfig.kt │ ├── utils │ ├── custom │ │ └── ffmpeg │ │ │ ├── ProcessListener.kt │ │ │ └── CustomRunProcessFunc.kt │ └── MatcherExtractor.kt │ ├── PodcastServerApplication.kt │ ├── messaging │ └── MessagingConfig.kt │ ├── podcast │ └── type │ │ ├── TypeConfig.kt │ │ └── TypeHandler.kt │ └── playlist │ └── Playlist.kt ├── .idea └── icon.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── distribution ├── kubernetes │ ├── base │ │ ├── components │ │ │ ├── namespace │ │ │ │ ├── namespace.yaml │ │ │ │ └── kustomization.yaml │ │ │ ├── ingress │ │ │ │ └── kustomization.yaml │ │ │ ├── storage │ │ │ │ └── embedded │ │ │ │ │ ├── gateway │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ ├── console │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ └── console.yaml │ │ │ │ │ └── storage.yaml │ │ │ │ │ ├── ingress │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ └── minio-console.yaml │ │ │ │ │ ├── backend-set-internal-storage.yaml │ │ │ │ │ └── kustomization.yaml │ │ │ ├── gateway │ │ │ │ ├── kustomization.yaml │ │ │ │ ├── backend.yaml │ │ │ │ └── frontend.yaml │ │ │ ├── database │ │ │ │ └── kustomization.yaml │ │ │ └── backend │ │ │ │ └── remote-debug │ │ │ │ └── kustomization.yaml │ │ ├── kustomization.yaml │ │ └── ui-v1.yaml │ ├── .infrastructure │ │ ├── dns │ │ │ └── podcast-server.profile │ │ ├── contour │ │ │ └── kustomization.yaml │ │ └── gateway │ │ │ ├── kustomization.yaml │ │ │ └── namereference.yaml │ └── overlays │ │ ├── docker-for-desktop │ │ ├── ingress │ │ │ ├── minio │ │ │ │ └── kustomization.yaml │ │ │ ├── podcast-server │ │ │ │ └── kustomization.yaml │ │ │ └── certs │ │ │ │ └── add-tls.yaml │ │ └── kustomization.yaml │ │ ├── all-in-one │ │ └── kustomization.yaml │ │ └── podcast.k8s.local │ │ ├── kustomization.yaml │ │ └── gateways │ │ └── kustomization.yaml └── kpt │ └── podcast-server │ ├── fn.yaml │ ├── storage │ ├── backend.storage.yaml │ ├── fs.storage.yaml │ └── database.storage.yaml │ └── ingress │ └── ingress.yaml ├── fake-external-podcast ├── src │ ├── podcast │ │ └── fake.jpg │ ├── conf │ │ └── default.conf │ └── docker │ │ └── Dockerfile └── build.sh ├── documentation ├── modules │ └── ROOT │ │ ├── pages │ │ └── introduction.adoc │ │ ├── nav.adoc │ │ └── examples │ │ └── installation │ │ └── kubernetes │ │ └── kustomization.yaml ├── antora.yml ├── supplemental-ui │ └── partials │ │ └── header-content.hbs └── documentation.yml ├── backend-lib-youtubedl └── src │ ├── test │ ├── resources │ │ └── youtube-dl │ └── java │ │ └── com │ │ └── gitlab │ │ └── davinkevin │ │ └── podcastserver │ │ └── youtubedl │ │ ├── YoutubeDLResponseTest.java │ │ └── YoutubeDLRequestTest.java │ └── main │ └── java │ └── com │ └── gitlab │ └── davinkevin │ └── podcastserver │ └── youtubedl │ ├── DownloadProgressCallback.java │ ├── mapper │ ├── VideoSubtitle.java │ ├── VideoThumbnail.java │ ├── HttpHeader.java │ └── VideoFormat.java │ ├── utils │ └── StreamGobbler.java │ └── YoutubeDLException.java ├── ui ├── src │ └── docker │ │ ├── Dockerfile │ │ └── config.toml └── compile.sh ├── backend-lib-database └── src │ └── main │ ├── migrations │ ├── V3__item-status-number-of-fail-not-null.sql │ ├── V6__add-guid-support-for-items.sql │ ├── V8__increase-multiple-varchar-size.sql │ ├── V5__use-database-for-download-manager.sql │ ├── V4__cover-url-not-null.sql │ ├── V10__add-cover-to-playlist.sql │ ├── V7__migrate-to-enums-to-items.sql │ └── V9__rename_watchlist_to_playlist.sql │ ├── docker │ └── Dockerfile │ └── java │ └── com │ └── github │ └── davinkevin │ └── podcastserver │ └── database │ └── routines │ ├── UuidNil.java │ ├── UuidNsDns.java │ ├── UuidNsOid.java │ ├── UuidNsUrl.java │ ├── UuidNsX500.java │ ├── UuidGenerateV1.java │ ├── UuidGenerateV4.java │ └── UuidGenerateV1mc.java ├── .envrc ├── .gitlab ├── ci │ └── gradle-build-tool.yaml └── renovate.json5 └── .gitignore /frontend-angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-angular/.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /frontend-angularjs/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.jvm.target.validation.mode = IGNORE 2 | -------------------------------------------------------------------------------- /storage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM minio/minio:RELEASE.2025-09-07T16-13-09Z 2 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/mimeTypeService/plain.text.txt: -------------------------------------------------------------------------------- 1 | plain.text -------------------------------------------------------------------------------- /frontend-angular/src/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $global-background-color: #fafafa; 3 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/.idea/icon.png -------------------------------------------------------------------------------- /backend/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /backend/src/test/resources/spring.properties: -------------------------------------------------------------------------------- 1 | spring.test.enclosing.configuration=OVERRIDE 2 | -------------------------------------------------------------------------------- /backend/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | #junit.jupiter.testinstance.lifecycle.default = per_class -------------------------------------------------------------------------------- /frontend-angular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/frontend-angular/src/favicon.ico -------------------------------------------------------------------------------- /frontend-angularjs/.envrc: -------------------------------------------------------------------------------- 1 | PATH_add .gradle/nodejs/node-v6.2.0-darwin-x64/bin/ 2 | PATH_add ./.gradle/npm/npm-v2.15.6/bin/ 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/namespace/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: podcast-server 5 | -------------------------------------------------------------------------------- /fake-external-podcast/src/podcast/fake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/fake-external-podcast/src/podcast/fake.jpg -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/modules/angularNotification.js: -------------------------------------------------------------------------------- 1 | import 'angular-notification'; 2 | 3 | export default { name : 'notification' }; -------------------------------------------------------------------------------- /backend/src/test/resources/remote/downloader/rtmpdump/rtmpdump.txt: -------------------------------------------------------------------------------- 1 | Progression : (1%) 2 | Progression : (2%) 3 | Progression : (3%) 4 | Download Complete -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/introduction.adoc: -------------------------------------------------------------------------------- 1 | = Introduction 2 | 3 | This site is the complement documentation of the Podcast-Server project. 4 | -------------------------------------------------------------------------------- /frontend-angular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | 5 | export const devTools = []; 6 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/backend/src/test/resources/__files/img/image.png -------------------------------------------------------------------------------- /frontend-angular/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/test/resources/youtube-dl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/HEAD/backend-lib-youtubedl/src/test/resources/youtube-dl -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/ionicons/ionicons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 31/10/2015 for PodcastServer 3 | */ 4 | import './ionicons.css!'; 5 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/ionicons/ionicons.less: -------------------------------------------------------------------------------- 1 | @import "/jspm_packages/github/driftyco/ionicons@2.0.1/less/ionicons"; 2 | @ionicons-font-path: '/fonts/'; -------------------------------------------------------------------------------- /distribution/kubernetes/.infrastructure/dns/podcast-server.profile: -------------------------------------------------------------------------------- 1 | 127.0.0.1 podcast.k8s.local minio.podcast.k8s.local 2 | ::1 podcast.k8s.local minio.podcast.k8s.local -------------------------------------------------------------------------------- /documentation/antora.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | title: Documentation 3 | version: "1.0.0" 4 | start_page: introduction.adoc 5 | nav: 6 | - modules/ROOT/nav.adoc 7 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/styles/styles.js: -------------------------------------------------------------------------------- 1 | import './bootstrap-adaptation.css!'; 2 | import './podcastserver.css!'; 3 | import './tags-input-bootstrap.css!'; -------------------------------------------------------------------------------- /documentation/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | 2 | * xref:introduction.adoc[Introduction] 3 | 4 | * *Installation* 5 | 6 | ** xref:installation/kubernetes.adoc[on kubernetes] 7 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/font-awesome/font-awesome.less: -------------------------------------------------------------------------------- 1 | 2 | @import "/jspm_packages/npm/font-awesome@4.5.0/less/font-awesome"; 3 | @fa-font-path : '/fonts/'; 4 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/ingress/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - ingress.yaml 6 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/bootstrap/bootstrap.less: -------------------------------------------------------------------------------- 1 | 2 | @import "/jspm_packages/github/distros/bootstrap-less@3.3.9/bootstrap/index"; 3 | @icon-font-path : '/fonts/'; 4 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/font-awesome/font-awesome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 31/10/2015 for PodcastServer 3 | */ 4 | 5 | import './font-awesome.css!'; -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/namespace/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - namespace.yaml 6 | -------------------------------------------------------------------------------- /ui/src/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM joseluisq/static-web-server:2.38.1 2 | 3 | COPY podcast-server /podcast-server 4 | COPY config.toml / 5 | 6 | ENV SERVER_CONFIG_FILE=config.toml 7 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/gateway/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - storage.yaml 6 | -------------------------------------------------------------------------------- /distribution/kubernetes/.infrastructure/contour/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - contour-gateway-provisioner.yaml -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/updating/updating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/gateway/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - backend.yaml 6 | - frontend.yaml 7 | 8 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/gateway/console/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - console.yaml 6 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/ingress/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - minio-console.yaml 6 | -------------------------------------------------------------------------------- /frontend-angularjs/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | # Author: DAVIN Kevin davin.kevin@gmail.com 3 | 4 | COPY dist /var/www/podcast-server 5 | COPY default.conf /etc/nginx/conf.d/default.conf 6 | -------------------------------------------------------------------------------- /frontend-angularjs/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* Full config of gulp task located in ./build/tasks/*.js */ 2 | 3 | import requiredir from 'require-dir'; 4 | 5 | requiredir('./gulp/tasks', { recurse: true }); -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/modules/truncate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 25/10/2015 for PodcastServer 3 | */ 4 | import 'angular-truncate'; 5 | 6 | export default { name : 'truncate' }; -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/dailymotion/karimdebbache.ids.0.item.json: -------------------------------------------------------------------------------- 1 | { 2 | "page":1, 3 | "limit":10, 4 | "explicit":false, 5 | "total":44, 6 | "has_more":true, 7 | "list":[] 8 | } 9 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/styles/bootstrap-adaptation.less: -------------------------------------------------------------------------------- 1 | .ps { 2 | .media-left, .media-right, .media-body { 3 | display: block; 4 | } 5 | 6 | .media-body { 7 | width: auto; 8 | } 9 | } -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/modules/ngTagsInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 25/10/2015 for PodcastServer 3 | */ 4 | 5 | import 'ng-tags-input'; 6 | 7 | export default { name : 'ngTagsInput' }; 8 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-tools-from-homebrew.yml: -------------------------------------------------------------------------------- 1 | podcastserver: 2 | externaltools: 3 | #rtmpdump: /usr/local/bin/rtmpdump 4 | ffmpeg: /opt/homebrew/bin/ffmpeg 5 | ffprobe: /opt/homebrew/bin/ffprobe 6 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/tag/Tag.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.tag 2 | 3 | import java.util.* 4 | 5 | data class Tag( 6 | val id: UUID, 7 | val name: String 8 | ) 9 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/dailymotion/karimdebbache-without-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar_720_url":"http:\/\/s2.dmcdn.net\/PB4mc\/720x720-AdY.jpg", 3 | "description":null, 4 | "username":"karimdebbache" 5 | } 6 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/docker-for-desktop/ingress/minio/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | components: 5 | - ../../../../base/components/storage/embedded/ingress 6 | -------------------------------------------------------------------------------- /fake-external-podcast/src/conf/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | gzip_static on; 5 | 6 | location / { 7 | autoindex on; 8 | root /var/www/podcast/; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend-angularjs/gulp/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angularjs-jspm-seed 3 | * Created by kdavin on 20/11/2015. 4 | */ 5 | 6 | Array.prototype.flatMap = function(lambda) { 7 | return Array.prototype.concat.apply([], this.map(lambda)); 8 | }; -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/htmlService/jsoup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSOUP Example 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/docker-for-desktop/ingress/podcast-server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | components: 5 | - ../../../../base/components/ingress 6 | 7 | 8 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/jdomService/invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tove 4 | Jani 5 | Reminder 6 | Don't forget me this weekend! -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters 2 | 3 | /** 4 | * Created by kevin on 25/12/2017 5 | */ 6 | data class Type(val key: String, val name: String) 7 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/jdomService/valid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tove 4 | Jani 5 | Reminder 6 | Don't forget me this weekend! 7 | -------------------------------------------------------------------------------- /backend/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "podcastserver.api.youtube ", 5 | "type": "java.lang.String", 6 | "description": "Youtube API Key" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /backend/src/main/resources/application-local-pg.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | username: ${PG_ALTERNATE_USER:podcast-server-user} 4 | password: ${PG_ALTERNATE_PASSWORD:nAAdo5wNs7WEF1UxUobpJDfS9Si62PHa} 5 | url: jdbc:postgresql://postgres:5432/podcast-server -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/DownloadProgressCallback.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl; 2 | 3 | public interface DownloadProgressCallback { 4 | void onProgressUpdate(float progress); 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-local-minio.yml: -------------------------------------------------------------------------------- 1 | podcastserver: 2 | storage: 3 | bucket: podcasts 4 | username: ${MINIO_ALTERNATE_USER:podcast-server-user} 5 | password: ${MINIO_ALTERNATE_PASSWORD:nAAdo5wNs7WEF1UxUobpJDfS9Si62PHa} 6 | url: http://localhost:9000 -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/dailymotion/karimdebbache.ids.1.items.json: -------------------------------------------------------------------------------- 1 | { 2 | "page":1, 3 | "limit":10, 4 | "explicit":false, 5 | "total":44, 6 | "has_more":true, 7 | "list":[ 8 | { 9 | "id":"x5ikng3" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/authorize-notification/authorize-notification.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V3__item-status-number-of-fail-not-null.sql: -------------------------------------------------------------------------------- 1 | UPDATE ITEM SET NUMBER_OF_FAIL = 0 WHERE NUMBER_OF_FAIL IS NULL; 2 | ALTER TABLE ITEM ALTER COLUMN NUMBER_OF_FAIL SET NOT NULL; 3 | ALTER TABLE ITEM ALTER COLUMN NUMBER_OF_FAIL SET DEFAULT 0; 4 | -------------------------------------------------------------------------------- /frontend-angular/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8080", 4 | "secure": false, 5 | "ws": true 6 | }, 7 | "/ws": { 8 | "secure": false, 9 | "target": "http://localhost:8080", 10 | "ws": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/docker-for-desktop/ingress/certs/add-tls.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: not-important 5 | spec: 6 | tls: 7 | - hosts: 8 | - to-be-replaced 9 | secretName: to-be-replaced 10 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/modules/highCharts.js: -------------------------------------------------------------------------------- 1 | import {Module} from '../../decorators'; 2 | import 'highcharts'; 3 | import HighChartsNg from 'highcharts-ng'; 4 | 5 | @Module({ name : 'ps.config.highCharts', modules : [ HighChartsNg ] }) 6 | export default class HighCharts{} -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/all-in-one/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | components: 8 | - ../../base/components/database 9 | - ../../base/components/storage/embedded 10 | -------------------------------------------------------------------------------- /frontend-angular/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend-angular/proxy-prod.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "https://podcast-server.tk", 4 | "secure": false, 5 | "ws": true 6 | }, 7 | "/ws": { 8 | "secure": false, 9 | "target": "https://podcast-server.tk", 10 | "ws": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend-angularjs/gulp/tasks/lint.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import paths from '../paths'; 3 | import eslint from 'gulp-eslint'; 4 | 5 | gulp.task('lint-js', () => { 6 | return gulp.src([paths.glob.js]) 7 | .pipe(eslint()) 8 | .pipe(eslint.format()); 9 | }); 10 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/extension/java/util/Optional.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.java.util 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Created by kevin on 02/11/2019 7 | */ 8 | fun Optional.orNull(): T? = this.orElse(null) 9 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/rss/Rss.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.rss 2 | 3 | import org.jdom2.Element 4 | 5 | fun rootRss(channel: Element): Element = Element("rss").apply { 6 | addContent(channel) 7 | addNamespaceDeclaration(itunesNS) 8 | } -------------------------------------------------------------------------------- /frontend-angular/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class PodcastServerPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('ps-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fake-external-podcast/src/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | # Author: DAVIN Kevin davin.kevin@gmail.com 3 | 4 | COPY default.conf /etc/nginx/conf.d/default.conf 5 | COPY podcast /var/www/podcast 6 | 7 | RUN chmod +x /var/www/podcast/create-podcast.bash && /var/www/podcast/create-podcast.bash 8 | -------------------------------------------------------------------------------- /frontend-angular/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-copy/vg-copy.less: -------------------------------------------------------------------------------- 1 | vg-copy { 2 | display: table-cell; 3 | width: 50px; 4 | vertical-align: middle; 5 | text-align: center; 6 | cursor: pointer; 7 | a, a:hover, a:visited { 8 | color: white; 9 | text-decoration: none; 10 | } 11 | } -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-link/vg-link.less: -------------------------------------------------------------------------------- 1 | vg-link { 2 | display: table-cell; 3 | width: 50px; 4 | vertical-align: middle; 5 | text-align: center; 6 | cursor: pointer; 7 | a, a:hover, a:visited { 8 | color: white; 9 | text-decoration: none; 10 | } 11 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/Cover.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.cover 2 | 3 | import java.net.URI 4 | import java.util.* 5 | 6 | data class Cover( 7 | val id: UUID, 8 | val url: URI, 9 | val height: Int, 10 | val width: Int 11 | ) 12 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.component.html: -------------------------------------------------------------------------------- 1 | 2 | Podcasts 3 | 4 | 5 |
6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-link-vlc/vg-link-vlc.less: -------------------------------------------------------------------------------- 1 | vg-link-vlc { 2 | display: table-cell; 3 | width: 50px; 4 | vertical-align: middle; 5 | text-align: center; 6 | cursor: pointer; 7 | a, a:hover, a:visited { 8 | color: white; 9 | text-decoration: none; 10 | } 11 | } -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/mapper/VideoSubtitle.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl.mapper; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class VideoSubtitle { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/extension/assertthat/SoftAsserts.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.assertthat 2 | 3 | import org.assertj.core.api.SoftAssertions 4 | 5 | fun assertAll(block: SoftAssertions.() -> Unit) = 6 | SoftAssertions() 7 | .apply(block) 8 | .assertAll() 9 | -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/fn.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: build 5 | annotations: 6 | config.kubernetes.io/function: | 7 | container: 8 | image: gcr.io/kpt-functions/kustomize-build 9 | network: 10 | required: true 11 | data: 12 | path: /source 13 | -------------------------------------------------------------------------------- /frontend-angular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | mat-sidenav-container { 2 | height: 100vh; 3 | } 4 | 5 | .spacer { 6 | flex: 1 1 auto; 7 | } 8 | 9 | .section-icon { 10 | padding: 0 14px; 11 | 12 | &[fontSet=fa]{ 13 | font-size: 24px; 14 | } 15 | 16 | } 17 | 18 | a[mat-list-item] { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /frontend-angularjs/gulp/tasks/fonts.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import flatten from 'gulp-flatten'; 3 | import paths from '../paths'; 4 | 5 | gulp.task('fonts', () => 6 | gulp.src([paths.jspm.fonts, paths.glob.projectFonts, '!'+paths.glob.fonts]) 7 | .pipe(flatten()) 8 | .pipe(gulp.dest(paths.app.fonts)) 9 | ); -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/filter/html2plainText.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | export default angular.module('ps.common.filter.html2plainText', []) 4 | .filter('htmlToPlaintext', () => (text) => String(text || "").replace(/<[^>]+>/gm, '')) 5 | .filter('cleanHtml', () => (text) => String(text || "").replace(' =""', '')); -------------------------------------------------------------------------------- /documentation/supplemental-ui/partials/header-content.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/item-menu/item-menu.less: -------------------------------------------------------------------------------- 1 | item-menu { 2 | .dropdown-menu > li { 3 | a.with-link { 4 | display: inline-block; 5 | padding-right:8px; 6 | } 7 | a + a[copy] { 8 | display: inline-block; 9 | width: 30px; 10 | padding: 3px 6px 3px 8px; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/database/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - database.yaml 6 | 7 | configMapGenerator: 8 | - name: database 9 | behavior: merge 10 | literals: 11 | - username=podcast-server-user 12 | - name=podcast-server 13 | -------------------------------------------------------------------------------- /frontend-angular/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 140, 3 | tabWidth: 2, 4 | useTabs: true, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'none', // other options `es5` or `all` 8 | bracketSpacing: true, 9 | arrowParens: 'avoid', // other option 'always' 10 | parser: 'typescript', 11 | }; 12 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/gateway/console/console.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1beta1 2 | kind: HTTPRoute 3 | metadata: 4 | name: console 5 | spec: 6 | rules: 7 | - matches: 8 | - path: 9 | value: "/" 10 | backendRefs: 11 | - name: storage 12 | port: 9001 -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/gateway/storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1beta1 2 | kind: HTTPRoute 3 | metadata: 4 | name: storage 5 | spec: 6 | rules: 7 | - matches: 8 | - path: 9 | value: "/dev-podcasts" 10 | backendRefs: 11 | - name: storage 12 | port: 9000 -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/rss/Namespaces.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.rss 2 | 3 | import org.jdom2.Namespace 4 | 5 | val itunesNS: Namespace = Namespace.getNamespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd") 6 | val mediaNS: Namespace = Namespace.getNamespace("media", "http://search.yahoo.com/mrss/") 7 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/loading.js: -------------------------------------------------------------------------------- 1 | import {Module, Config} from '../decorators'; 2 | import 'angular-loading-bar'; 3 | 4 | @Module({ 5 | name : 'ps.config.loading', 6 | modules : [ 'angular-loading-bar' ] 7 | }) 8 | @Config(cfpLoadingBarProvider => { "ngInject"; cfpLoadingBarProvider.includeSpinner = false; }) 9 | export default class LoadingBar {} -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/extension/java/net/URI.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.java.net 2 | 3 | import java.net.URI 4 | import kotlin.io.path.Path 5 | import kotlin.io.path.extension 6 | 7 | /** 8 | * Created by kevin on 12/07/2020 9 | */ 10 | fun URI.extension(): String = Path(this.path).extension 11 | -------------------------------------------------------------------------------- /backend/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/backend-set-internal-storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: app 10 | env: 11 | - name: PODCASTSERVER_STORAGE_IS_INTERNAL 12 | value: "true" 13 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/navbar/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/tag/TagService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.tag 2 | 3 | import java.util.* 4 | 5 | class TagService(private val repository: TagRepository) { 6 | fun findById(id: UUID): Tag? = repository.findById(id) 7 | fun findByNameLike(name: String): List = repository.findByNameLike(name) 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/youtube/joueurdugrenier.id.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "youtube#channelListResponse", 3 | "etag": "V6f8V0AHAyAtwJCBr5pFW9Y08f8", 4 | "pageInfo": {"totalResults": 1, "resultsPerPage": 5}, 5 | "items": [{ 6 | "kind": "youtube#channel", 7 | "etag": "P_Oq-TaAXb4OaAd4_3j2jDMUwAw", 8 | "id": "UC_yP2DpIgs5Y1uWC0T03Chw" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/mapper/VideoThumbnail.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl.mapper; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class VideoThumbnail { 7 | public String url; 8 | public String id; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/rtmp/RTMPDownloaderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders.rtmp 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.context.annotation.Import 5 | 6 | @Configuration 7 | @Import(RTMPDownloaderFactory::class) 8 | class RTMPDownloaderConfig -------------------------------------------------------------------------------- /frontend-angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PodcastServer 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/item/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 01/11/14 for Podcast Server 3 | */ 4 | import {Module} from '../decorators'; 5 | import ItemDetailsModule from './details/item.details'; 6 | import ItemPlayer from './player/item.player'; 7 | 8 | @Module({ 9 | name : 'ps.item', 10 | modules : [ItemDetailsModule, ItemPlayer] 11 | }) 12 | export default class Item{} -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V6__add-guid-support-for-items.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE ITEM 2 | ADD COLUMN GUID TEXT, 3 | DROP CONSTRAINT item_podcast_id_url_key 4 | ; 5 | 6 | UPDATE ITEM SET GUID=COALESCE(ITEM.URL, ITEM.ID::text); 7 | 8 | ALTER TABLE ITEM 9 | ADD CONSTRAINT ITEM_WITH_GUID_IS_UNIQUE_IN_PODCAST UNIQUE (GUID, PODCAST_ID), 10 | ALTER COLUMN GUID SET NOT NULL; 11 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/bootstrap/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 25/10/2015 for PodcastServer 3 | */ 4 | 5 | import './bootstrap.css!'; 6 | import uiBootstrap from 'angular-bootstrap'; 7 | import {Module} from '../../decorators'; 8 | 9 | @Module({ 10 | name : 'ps.config.bootstrap', 11 | modules : [ uiBootstrap ] 12 | }) 13 | export default class Bootstrap{} -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/ffmpeg/FfmpegDownloaderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders.ffmpeg 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.context.annotation.Import 5 | 6 | @Configuration 7 | @Import(FfmpegDownloaderFactory::class) 8 | class FfmpegDownloaderConfig -------------------------------------------------------------------------------- /frontend-angular/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /ui/src/docker/config.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | host = "::" 4 | port = 8080 5 | root = "/podcast-server" 6 | log-level = "error" 7 | 8 | page-fallback = "/podcast-server/index.html" 9 | cache-control-headers = true 10 | compression = true 11 | compression-static = true 12 | security-headers = true 13 | directory-listing = false 14 | redirect-trailing-slash = true 15 | ignore-hidden-files = true 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/database/StatusConverter.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.database 2 | 3 | import com.github.davinkevin.podcastserver.entity.Status 4 | import org.jooq.impl.EnumConverter 5 | 6 | /** 7 | * Created by kevin on 28/12/2019 8 | */ 9 | class StatusConverter: EnumConverter(String::class.java, Status::class.java) 10 | -------------------------------------------------------------------------------- /distribution/kubernetes/.infrastructure/gateway/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: projectcontour 5 | 6 | resources: 7 | - gateway.yaml 8 | 9 | secretGenerator: 10 | - name: dot-k8s-dot-local 11 | type: kubernetes.io/tls 12 | files: 13 | - tls.crt 14 | - tls.key 15 | 16 | configurations: 17 | - namereference.yaml -------------------------------------------------------------------------------- /distribution/kubernetes/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - backend.yaml 6 | - ui-v1.yaml 7 | - ui-v2.yaml 8 | 9 | configMapGenerator: 10 | - name: podcast-server 11 | - name: database 12 | - name: storage 13 | 14 | secretGenerator: 15 | - name: podcast-server 16 | - name: database 17 | - name: storage 18 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/config/ClockConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.config 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import java.time.Clock 6 | 7 | @Configuration 8 | class ClockConfig { 9 | 10 | @Bean fun clock(): Clock = Clock.systemDefaultZone() 11 | 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/noop/NoopConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.noop 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.context.annotation.Import 5 | 6 | /** 7 | * Created by kevin on 02/11/2019 8 | */ 9 | @Configuration 10 | @Import(NoOpFinder::class) 11 | class NoopConfig 12 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # If you want to use Gradle Enterprise, set the following value to true 2 | export DEVELOCITY_ENABLED=false 3 | 4 | # URL of your Gradle Enterprise instance: 5 | export DEVELOCITY_SERVER=https://develocity.your-instance.com/ 6 | 7 | # If you want to use Gradle Enterprise Predictive Selection, set the following value to true 8 | export PREDICTIVE_TEST_SELECTION_ENABLED=false 9 | 10 | dotenv_if_exists .env.local 11 | -------------------------------------------------------------------------------- /frontend-angular/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { PodcastServerPage } from './app.po'; 2 | 3 | describe('podcast-server App', () => { 4 | let page: PodcastServerPage; 5 | 6 | beforeEach(() => { 7 | page = new PodcastServerPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('ps works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/dailymotion/karimdebbache.json: -------------------------------------------------------------------------------- 1 | { 2 | "avatar_720_url":"http:\/\/s2.dmcdn.net\/PB4mc\/720x720-AdY.jpg", 3 | "description":"CHROMA est une CHROnique de cinéMA sur Dailymotion, dont la première saison se compose de dix épisodes, à raison d’un par mois, d’une durée comprise entre quinze et vingt minutes. Chaque épisode est consacré à un film en particulier.", 4 | "username":"karimdebbache" 5 | } -------------------------------------------------------------------------------- /frontend-angular/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "../out-tsc/spec", 6 | "baseUrl": "./", 7 | "module": "commonjs", 8 | "types": [ 9 | "jest", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/podcasts/details/upload/upload.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
7 |
8 | Drop one or many files here 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/test/java/com/gitlab/davinkevin/podcastserver/youtubedl/YoutubeDLResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | public class YoutubeDLResponseTest { 8 | 9 | @Test 10 | public void testTest() { 11 | assertEquals(true, true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /documentation/documentation.yml: -------------------------------------------------------------------------------- 1 | site: 2 | title: Podcast Server Documentation 3 | start_page: documentation::introduction.adoc 4 | content: 5 | sources: 6 | - url: ./../ 7 | start_path: documentation 8 | branches: HEAD 9 | ui: 10 | bundle: 11 | url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable 12 | supplemental_files: ./supplemental-ui 13 | -------------------------------------------------------------------------------- /frontend-angularjs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "linebreak-style": [ 2, "unix" ], 5 | "semi": [ 2, "always" ] 6 | }, 7 | "env": { 8 | "es6": true, 9 | "browser": true, 10 | "jasmine": true 11 | }, 12 | "globals": { 13 | "inject": true, 14 | "module": true 15 | }, 16 | "extends": "eslint:recommended", 17 | "ecmaFeatures": { 18 | "modules" : true 19 | } 20 | } -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/gateway/backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1beta1 2 | kind: HTTPRoute 3 | metadata: 4 | name: backend 5 | spec: 6 | rules: 7 | - matches: 8 | - path: 9 | value: "/api" 10 | - path: 11 | value: "/actuator" 12 | - path: 13 | value: "/kodi" 14 | backendRefs: 15 | - name: backend 16 | port: 8080 17 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V8__increase-multiple-varchar-size.sql: -------------------------------------------------------------------------------- 1 | alter table item 2 | alter column file_name type varchar(65535) using file_name::varchar(65535), 3 | alter column title type varchar(65535) using title::varchar(65535); 4 | 5 | alter table cover 6 | alter column url type varchar(65535) using url::varchar(65535); 7 | 8 | alter table podcast 9 | alter column title type varchar(65535) using title::varchar(65535); -------------------------------------------------------------------------------- /frontend-angular/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | @import '~font-awesome/css/font-awesome.css'; 4 | @import '~material-design-icons/iconfont/material-icons.css'; 5 | @import "variables"; 6 | 7 | body { 8 | margin: 0; 9 | font-family: Roboto, sans-serif; 10 | background-color: $global-background-color; 11 | } 12 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - storage.yaml 6 | 7 | patchesStrategicMerge: 8 | - backend-set-internal-storage.yaml 9 | 10 | configMapGenerator: 11 | - name: storage 12 | behavior: merge 13 | literals: 14 | - username=podcast-server-user 15 | - bucket=data 16 | - url=http://storage:9000/ 17 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V5__use-database-for-download-manager.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE DOWNLOADING_STATE AS ENUM ('WAITING', 'DOWNLOADING'); 2 | 3 | CREATE TABLE DOWNLOADING_ITEM ( 4 | ITEM_ID UUID NOT NULL, 5 | POSITION INT NOT NULL, 6 | STATE DOWNLOADING_STATE NOT NULL DEFAULT 'WAITING', 7 | 8 | PRIMARY KEY(ITEM_ID), 9 | FOREIGN KEY(ITEM_ID) REFERENCES ITEM(ID), 10 | UNIQUE (POSITION) DEFERRABLE INITIALLY DEFERRED 11 | ) 12 | -------------------------------------------------------------------------------- /distribution/kubernetes/.infrastructure/gateway/namereference.yaml: -------------------------------------------------------------------------------- 1 | nameReference: 2 | - kind: Secret 3 | version: v1 4 | fieldSpecs: 5 | - apiVersion: gateway.networking.k8s.io/v1beta1 6 | kind: Gateway 7 | path: spec/listeners/tls/certificateRefs/name 8 | - kind: Service 9 | fieldSpecs: 10 | - apiVersion: gateway.networking.k8s.io/v1beta1 11 | kind: HTTPRoute 12 | path: /spec/rules/backendRefs/name 13 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/device-detection.js: -------------------------------------------------------------------------------- 1 | import {Service, Module} from '../../decorators'; 2 | 3 | @Module({ name : 'ps.common.component.device-detection' }) 4 | @Service('deviceDetectorService') 5 | export default class DeviceDetectorService { 6 | constructor($window) { 7 | "ngInject"; 8 | this.$window = $window; 9 | } 10 | 11 | isTouchedDevice() { 12 | return 'ontouchstart' in this.$window; 13 | } 14 | } -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/storage/embedded/ingress/minio-console.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: minio-console 5 | spec: 6 | rules: 7 | - host: to-be-replaced 8 | http: 9 | paths: 10 | - pathType: Prefix 11 | path: / 12 | backend: 13 | service: 14 | name: storage 15 | port: 16 | number: 9001 17 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/database/PathConverter.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.database 2 | 3 | import org.jooq.Converter 4 | import java.nio.file.Path 5 | import kotlin.io.path.Path 6 | 7 | /** 8 | * Created by kevin on 18/06/2022 9 | */ 10 | private val innerConverter = Converter.ofNullable(String::class.java, Path::class.java, ::Path, Path::toString) 11 | class PathConverter: Converter by innerConverter 12 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/gateway/frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1beta1 2 | kind: HTTPRoute 3 | metadata: 4 | name: frontend 5 | spec: 6 | rules: 7 | - matches: 8 | - path: 9 | value: "/v2" 10 | backendRefs: 11 | - name: ui-v2 12 | port: 8080 13 | - matches: 14 | - path: 15 | value: "/" 16 | backendRefs: 17 | - name: ui-v1 18 | port: 8080 19 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | dehaze 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /backend/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | 2 | spring: 3 | jooq: 4 | sql-dialect: postgres 5 | main: 6 | banner-mode: "off" 7 | threads: 8 | virtual: 9 | enabled: false 10 | server: 11 | error: 12 | include-message: always 13 | 14 | logging: 15 | level: 16 | org.springframework: "off" 17 | org.eclipse.jetty: "off" 18 | org.jooq.Constants: "off" 19 | 20 | podcastserver: 21 | storage: 22 | username: foo 23 | password: password 24 | -------------------------------------------------------------------------------- /fake-external-podcast/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCKER_FOLDER=fake-external-podcast 4 | 5 | rm -rf ${DOCKER_FOLDER}/target 6 | mkdir -p ${DOCKER_FOLDER}/target/docker 7 | 8 | cp -r ${DOCKER_FOLDER}/src/docker/Dockerfile \ 9 | ${DOCKER_FOLDER}/src/conf/default.conf \ 10 | ${DOCKER_FOLDER}/src/podcast \ 11 | ${DOCKER_FOLDER}/target/docker 12 | 13 | cd ${DOCKER_FOLDER}/target/docker/ 14 | docker build -t davinkevin/podcast-server/fake-external-podcast:latest . 15 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/title.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 07/05/2016. 3 | */ 4 | import {Module, Service} from '../../decorators'; 5 | import Rx from 'rx'; 6 | 7 | @Module({ name : 'ps.common.service.title' }) 8 | @Service('TitleService') 9 | export class TitleService { 10 | 11 | title$ = new Rx.BehaviorSubject('Podcast-Server'); 12 | 13 | set title(title) { this.title$.onNext(title); } 14 | get title() { return this.title$; } 15 | 16 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/extension/restclient/RestClient.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.restclient 2 | 3 | import org.springframework.http.converter.StringHttpMessageConverter 4 | import org.springframework.web.client.RestClient 5 | import java.nio.charset.Charset 6 | 7 | fun RestClient.Builder.withStringUTF8MessageConverter(): RestClient.Builder = 8 | this.messageConverters { it.addFirst(StringHttpMessageConverter(Charset.forName("UTF-8"))) } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/UpdaterSelector.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters 2 | 3 | import java.net.URI 4 | 5 | /** 6 | * Created by kevin on 06/03/15. 7 | */ 8 | class UpdaterSelector(val updaters: Set) { 9 | fun of(url: URI): Updater = updaters.minByOrNull { updater: Updater -> updater.compatibility(url.toASCIIString()) }!! 10 | fun types(): Set = updaters.map { it.type() }.toSet() 11 | } 12 | -------------------------------------------------------------------------------- /frontend-angularjs/docker/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | gzip_static on; 5 | 6 | location ~ (.*\.js|.*\.css|.*\.html|.*\.eot|.*\.svg|.*\.ttf|.*\.woff|.*\.woff2|.*\.ico) { 7 | root /var/www/podcast-server; 8 | } 9 | 10 | location ~ ^/v2/.* { 11 | root /var/www/podcast-server; 12 | try_files /v2/index.html =404; 13 | } 14 | location / { 15 | root /var/www/podcast-server; 16 | try_files /index.html =404; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/image/ImageServiceConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.image 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.web.client.RestClient 6 | 7 | @Configuration 8 | class ImageServiceConfig { 9 | 10 | @Bean 11 | fun imageServiceV2(wcb: RestClient.Builder): ImageService = ImageService(wcb.clone()) 12 | } 13 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/updating/updating.less: -------------------------------------------------------------------------------- 1 | .ps { 2 | update-status { 3 | 4 | @keyframes rotate { 5 | from { transform: rotate(0deg); } 6 | to { transform: rotate(360deg); } 7 | } 8 | 9 | .notUpdating { 10 | visibility: hidden; 11 | } 12 | 13 | i.updating { 14 | animation-name: rotate; 15 | animation-duration: 1s; 16 | animation-iteration-count: infinite; 17 | animation-timing-function: linear; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/kodi/Kodi.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.kodi 2 | 3 | import java.nio.file.Path 4 | import java.time.OffsetDateTime 5 | import java.util.* 6 | 7 | data class Podcast( 8 | val id: UUID, 9 | val title: String, 10 | ) 11 | 12 | data class Item( 13 | val id: UUID, 14 | val title: String, 15 | val pubDate: OffsetDateTime, 16 | 17 | val length: Long, 18 | val fileName: Path?, 19 | val mimeType: String, 20 | ) -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- 1 | mat-toolbar { 2 | width: 100%; 3 | display:flex; 4 | justify-content:flex-end; 5 | 6 | mat-icon { 7 | font-size: 1.3rem; 8 | width: inherit; 9 | height: 1.4rem; 10 | vertical-align: middle; 11 | } 12 | 13 | .toolbar__left { 14 | margin-right:auto; 15 | .toolbar__title { 16 | margin-left: 1vw; 17 | } 18 | } 19 | 20 | .toolbar__right { 21 | align-self:end; 22 | padding-top: 12px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend-angularjs/gulp/tasks/less.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import util from 'gulp-util'; 3 | import less from 'gulp-less'; 4 | import paths from '../paths'; 5 | 6 | function logError(err) { 7 | util.log(err); 8 | this.emit('end'); 9 | } 10 | 11 | gulp.task('less', () => 12 | gulp.src([paths.glob.less]) 13 | .pipe(less({ 14 | paths: [ paths.srcDir ] 15 | }) 16 | .on('error', logError)) 17 | .pipe(gulp.dest(paths.app.app)) 18 | ); -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/Finder.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders 2 | 3 | import com.github.davinkevin.podcastserver.find.FindPodcastInformation 4 | import org.slf4j.LoggerFactory 5 | 6 | private val log = LoggerFactory.getLogger(Finder::class.java) 7 | /** 8 | * Created by kevin on 22/02/15. 9 | */ 10 | interface Finder { 11 | fun findPodcastInformation(url: String): FindPodcastInformation? 12 | fun compatibility(url: String): Int 13 | } 14 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/data/updateService.js: -------------------------------------------------------------------------------- 1 | import {Module, Service} from '../../../decorators'; 2 | 3 | @Module({ 4 | name : 'ps.common.service.data.updateService' 5 | }) 6 | @Service('UpdateService') 7 | export default class UpdateService { 8 | constructor($http) { 9 | "ngInject"; 10 | this.$http = $http; 11 | } 12 | 13 | forceUpdatePodcast(idPodcast) { 14 | return this.$http.post('/api/task/updateManager/updatePodcast/force', idPodcast); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/FindPocastInformation.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find 2 | 3 | import java.net.URI 4 | 5 | /** 6 | * Created by kevin on 2019-08-11 7 | */ 8 | data class FindPodcastInformation( 9 | val title: String, 10 | val description: String = "", 11 | val url: URI, 12 | val cover: FindCoverInformation?, 13 | val type: String 14 | ) 15 | 16 | data class FindCoverInformation(val height: Int, val width: Int, val url: URI) 17 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { ItemService } from './service/item/item.service'; 5 | import { PodcastService } from './service/podcast/podcast.service'; 6 | import { ToolbarModule } from './toolbar/toolbar.module'; 7 | 8 | @NgModule({ 9 | imports: [HttpClientModule, ToolbarModule], 10 | exports: [ToolbarModule], 11 | providers: [ItemService, PodcastService] 12 | }) 13 | export class SharedModule {} 14 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YTDlpParameters.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders.youtubedl 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | // https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options 6 | @ConfigurationProperties("podcastserver.external-tools.yt-dlp") 7 | data class YTDlpParameters( 8 | val path: String = "/usr/local/bin/youtube-dl", 9 | val extraParameters: String = "{}", 10 | ) -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/storage/backend.storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: app 10 | volumeMounts: 11 | - name: podcasts-files 12 | mountPath: /podcasts 13 | volumes: 14 | - name: podcasts-files 15 | hostPath: 16 | path: /tmp/podcast-server/files # {"$ref":"#/definitions/io.k8s.cli.substitutions.install-location.files"} 17 | type: Directory 18 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ToolbarComponent } from './toolbar.component'; 4 | import { MatButtonModule, MatIconModule, MatMenuModule, MatToolbarModule } from '@angular/material'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule, MatIconModule, MatButtonModule, MatMenuModule, MatToolbarModule], 8 | exports: [ToolbarComponent], 9 | declarations: [ToolbarComponent] 10 | }) 11 | export class ToolbarModule {} 12 | -------------------------------------------------------------------------------- /ui/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | UI_FOLDER=ui 4 | 5 | rm -rf ${UI_FOLDER}/target 6 | mkdir -p ${UI_FOLDER}/target/docker 7 | 8 | echo "Injection of SWS configuration" 9 | cp -r ${UI_FOLDER}/src/docker/* ${UI_FOLDER}/target/docker/ 10 | 11 | echo "Injection of ui-v1 files" 12 | cp -r frontend-angularjs/target/dist ${UI_FOLDER}/target/docker/podcast-server 13 | 14 | echo "Injection of ui-v2 files" 15 | mkdir -p ${UI_FOLDER}/target/docker/podcast-server/v2/ 16 | cp frontend-angular/dist/* ${UI_FOLDER}/target/docker/podcast-server/v2/ 17 | -------------------------------------------------------------------------------- /.gitlab/ci/gradle-build-tool.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - local: .gitlab/ci/utils.yaml 3 | 4 | .with-build-tool: &with-build-tool 5 | key: 6 | files: 7 | - gradle/wrapper/gradle-wrapper.properties 8 | prefix: "build-tool" 9 | paths: 10 | - .gradle_home 11 | 12 | .using-build-tool: 13 | <<: *with-build-tool 14 | policy: pull 15 | 16 | ⬇ build-tool: 17 | stage: ⬇ download 18 | extends: [ .only-on-code-change ] 19 | image: gradle:jdk21 20 | script: 21 | - ./gradlew --version 22 | cache: 23 | - !reference [ .with-build-tool ] -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/storage/fs.storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: fs 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: fs 10 | volumeMounts: 11 | - name: podcasts-files 12 | mountPath: /var/www/podcast-server-files/data 13 | volumes: 14 | - name: podcasts-files 15 | hostPath: 16 | path: /tmp/podcast-server/files # {"$ref":"#/definitions/io.k8s.cli.substitutions.install-location.files"} 17 | type: Directory 18 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/podcast.component.scss: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | display: block; 3 | width:100%; 4 | height:400px; 5 | margin: auto; 6 | background-size: cover; 7 | background-repeat: no-repeat; 8 | background-position: center; 9 | 10 | .buttons { 11 | width: 100%; 12 | display:flex; 13 | justify-content:flex-start; 14 | color: white; 15 | padding: 1vh 1vh; 16 | 17 | & a { 18 | color: white; 19 | mat-icon { 20 | cursor: pointer; 21 | } 22 | } 23 | } 24 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-copy/vg-copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 20/02/2016 for Podcast Server 3 | */ 4 | import {Component, Module} from '../../../../decorators'; 5 | import './vg-copy.css!'; 6 | 7 | @Module({ 8 | name : 'ps.common.component.videogular.vgCopy' 9 | }) 10 | @Component({ 11 | selector : 'vg-copy', 12 | as : 'vgcopy', 13 | bindings : { url: '='}, 14 | template : `
` 15 | }) 16 | export default class VgCopy {} 17 | 18 | -------------------------------------------------------------------------------- /frontend-angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "paths": { 13 | "#app/*": ["app/*"] 14 | }, 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2017", 20 | "dom" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YoutubeDlUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders.youtubedl 2 | 3 | /** 4 | * Created by kevin on 08/05/2020 5 | */ 6 | 7 | internal fun isFromVideoPlatform(url: String): Boolean = when { 8 | "youtube.com" in url -> true 9 | "www.6play.fr" in url -> true 10 | "www.tf1.fr" in url -> true 11 | "www.france.tv" in url -> true 12 | "replay.gulli.fr" in url -> true 13 | "dailymotion.com" in url -> true 14 | else -> false 15 | } 16 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/data/typeService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 01/11/14 for Podcast Server 3 | */ 4 | import {Module, Service} from '../../../decorators'; 5 | 6 | @Module({ 7 | name : 'ps.common.service.data.typeService' 8 | }) 9 | @Service('typeService') 10 | export default class typeService { 11 | 12 | constructor($http) { 13 | "ngInject"; 14 | this.$http = $http; 15 | } 16 | 17 | findAll() { 18 | return this.$http.get('/api/v1/podcasts/types') 19 | .then(r => r.data.content); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/utils/custom/ffmpeg/ProcessListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils.custom.ffmpeg 2 | 3 | import java.util.concurrent.CompletableFuture 4 | 5 | 6 | class ProcessListener(val url: String) { 7 | 8 | companion object { 9 | val DEFAULT_PROCESS_LISTENER = ProcessListener("") 10 | } 11 | 12 | var process: CompletableFuture = CompletableFuture() 13 | 14 | fun withProcess(p: Process): ProcessListener { 15 | process.complete(p) 16 | return this 17 | } 18 | } -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.component.scss: -------------------------------------------------------------------------------- 1 | .podcasts__results { 2 | list-style: none; 3 | display: flex; 4 | margin-top: 8px; 5 | flex-flow: row wrap; 6 | justify-content: space-around; 7 | 8 | [mat-card-image] { 9 | width: 200px; 10 | height: 200px; 11 | margin: 3px; 12 | object-fit: cover; 13 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 14 | } 15 | } 16 | 17 | mat-icon { 18 | font-size: 1.3rem; 19 | width: inherit; 20 | height: 1.4rem; 21 | vertical-align: middle; 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/properties/ExternalTools.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.properties 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | /** 6 | * Created by kevin on 12/04/2016 for Podcast Server 7 | */ 8 | @ConfigurationProperties("podcastserver.externaltools") 9 | data class ExternalTools( 10 | val ffmpeg: String = "/usr/local/bin/ffmpeg", 11 | val ffprobe: String = "/usr/local/bin/ffprobe", 12 | val rtmpdump: String = "/usr/local/bin/rtmpdump", 13 | ) 14 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/navbar/navbar.less: -------------------------------------------------------------------------------- 1 | @import "/jspm_packages/github/distros/bootstrap-less@3.3.9/bootstrap/variables.less"; 2 | .ps { 3 | 4 | @media screen and (min-width: (@screen-sm-min)) { 5 | .navbar-fixed-top { 6 | position: relative; 7 | } 8 | } 9 | 10 | @media screen and (max-width: (@screen-sm-min - 1)) { 11 | body { padding-top: 55px; }; 12 | } 13 | 14 | .navbar { 15 | margin-bottom: 0; 16 | border-radius: 0; 17 | } 18 | 19 | .navbar-nav { 20 | & >li { 21 | float: left; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /frontend-angular/src/app/floating-player/floating-player.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Item } from '#app/shared/entity'; 3 | 4 | export enum FloatingPlayerAction { 5 | PLAY = '[FLOATING_PLAYER] Play', 6 | CLOSE = '[FLOATING_PLAYER] Close' 7 | } 8 | 9 | export class PlayAction implements Action { 10 | readonly type = FloatingPlayerAction.PLAY; 11 | constructor(public item: Item) {} 12 | } 13 | export class CloseAction implements Action { 14 | readonly type = FloatingPlayerAction.CLOSE; 15 | } 16 | 17 | export type FloatingPlayerActions = PlayAction | CloseAction; 18 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/upload/UploadUpdaterConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.upload 2 | 3 | import io.micrometer.core.instrument.MeterRegistry 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | 7 | /** 8 | * Created by kevin on 01/05/2020 9 | */ 10 | @Configuration 11 | class UploadUpdaterConfig { 12 | 13 | @Bean 14 | fun uploadUpdater( 15 | registry: MeterRegistry 16 | ): UploadUpdater = UploadUpdater(registry) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/data/tagService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 01/11/14 for Podcast Server 3 | */ 4 | import {Module, Service} from '../../../decorators'; 5 | 6 | @Module({ 7 | name : 'ps.common.service.data.tagService' 8 | }) 9 | @Service('tagService') 10 | export default class tagService { 11 | 12 | constructor($http) { 13 | "ngInject"; 14 | this.$http = $http; 15 | } 16 | 17 | search(query) { 18 | return this.$http 19 | .get(`/api/v1/tags/search?name=${query}`) 20 | .then(v => v.data.content); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-link/vg-link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 20/02/2016 for Podcast Server 3 | */ 4 | import {Component, Module} from '../../../../decorators'; 5 | import './vg-link.css!'; 6 | 7 | @Module({ 8 | name : 'ps.common.component.videogular.vgLink' 9 | }) 10 | @Component({ 11 | selector : 'vg-link', 12 | as : 'vglink', 13 | bindings : { url: '='}, 14 | template : `
` 15 | }) 16 | export default class VgLink {} 17 | 18 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V4__cover-url-not-null.sql: -------------------------------------------------------------------------------- 1 | UPDATE COVER SET URL='https://via.placeholder.com/200x200' WHERE URL IS NULL; 2 | ALTER TABLE COVER 3 | ALTER COLUMN URL SET NOT NULL, 4 | ALTER COLUMN URL SET DEFAULT 'https://via.placeholder.com/200x200'; 5 | 6 | UPDATE COVER SET HEIGHT = 200 WHERE HEIGHT IS NULL; 7 | ALTER TABLE COVER 8 | ALTER COLUMN HEIGHT SET NOT NULL, 9 | ALTER COLUMN HEIGHT SET DEFAULT 200; 10 | 11 | UPDATE COVER SET WIDTH = 200 WHERE WIDTH IS NULL; 12 | ALTER TABLE COVER 13 | ALTER COLUMN WIDTH SET NOT NULL, 14 | ALTER COLUMN WIDTH SET DEFAULT 200; 15 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Podcast } from '../shared/entity'; 3 | 4 | export enum PodcastsAction { 5 | FIND_ALL = '[Podcasts] Find all', 6 | FIND_ALL_SUCCESS = '[Podcasts] Find all Success' 7 | } 8 | 9 | export class FindAll implements Action { 10 | readonly type = PodcastsAction.FIND_ALL; 11 | } 12 | 13 | export class FindAllSuccess implements Action { 14 | readonly type = PodcastsAction.FIND_ALL_SUCCESS; 15 | 16 | constructor(public podcasts: Podcast[]) {} 17 | } 18 | 19 | export type PodcastsActions = FindAll | FindAllSuccess; 20 | -------------------------------------------------------------------------------- /frontend-angular/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | import {StoreDevtoolsModule} from '@ngrx/store-devtools'; 7 | 8 | export const environment = { 9 | production: false, 10 | }; 11 | 12 | export const devTools = [ 13 | StoreDevtoolsModule.instrument({maxAge: 25}) 14 | ]; 15 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { OpenSideNavAction } from '#app/app.actions'; 3 | import { AppState } from '#app/app.reducer'; 4 | import { Store } from '@ngrx/store'; 5 | 6 | @Component({ 7 | selector: 'ps-toolbar', 8 | templateUrl: './toolbar.component.html', 9 | styleUrls: ['./toolbar.component.scss'] 10 | }) 11 | export class ToolbarComponent implements OnInit { 12 | constructor(private store: Store) {} 13 | 14 | ngOnInit() {} 15 | 16 | openSideNav() { 17 | this.store.dispatch(new OpenSideNavAction()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V10__add-cover-to-playlist.sql: -------------------------------------------------------------------------------- 1 | create extension "uuid-ossp"; 2 | 3 | alter table playlist 4 | add column cover_id uuid, 5 | add constraint playlist_cover_id_fk foreign key (cover_id) references cover; 6 | 7 | with cover_creation as ( 8 | insert into cover (id, height, url, width) 9 | values (uuid_generate_v4(), 600, 'https://placehold.co/600x600?text=no+cover', 600) 10 | returning id 11 | ) 12 | update playlist 13 | set cover_id = (select id from cover_creation) 14 | where playlist.cover_id is null; 15 | 16 | alter table playlist alter column cover_id set not null; 17 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V7__migrate-to-enums-to-items.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE ITEM_STATUS AS ENUM ( 2 | 'NOT_DOWNLOADED', 3 | 'STARTED', 4 | 'PAUSED', 5 | 'DELETED', 6 | 'STOPPED', 7 | 'FAILED', 8 | 'FINISH' 9 | ); 10 | 11 | ALTER TABLE ITEM ADD COLUMN STATUS_TEXT VARCHAR(255); 12 | -- noinspection SqlWithoutWhere 13 | UPDATE ITEM SET STATUS_TEXT = STATUS; 14 | 15 | ALTER TABLE ITEM 16 | DROP COLUMN STATUS, 17 | ADD COLUMN STATUS ITEM_STATUS NOT NULL DEFAULT 'NOT_DOWNLOADED'; 18 | 19 | UPDATE ITEM 20 | SET STATUS = STATUS_TEXT::ITEM_STATUS; 21 | 22 | ALTER TABLE ITEM DROP COLUMN STATUS_TEXT; 23 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/ProcessService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service 2 | 3 | import org.springframework.stereotype.Service 4 | 5 | /** 6 | * Created by kevin on 25/01/2016 for Podcast Server 7 | */ 8 | @Service 9 | class ProcessService { 10 | 11 | fun newProcessBuilder(vararg command: String) = ProcessBuilder(*command) 12 | 13 | fun start(processBuilder: ProcessBuilder): Process = processBuilder.start() 14 | 15 | fun pidOf(p: Process) = p.pid() 16 | 17 | fun waitFor(process: Process): Result = Result.runCatching { process.waitFor() } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/PodcastServerApplication.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | /** 7 | * Created by kevin on 2019-02-09 8 | */ 9 | @SpringBootApplication 10 | class PodcastServerApplication 11 | 12 | fun main(args: Array) { 13 | System.getProperties().apply { 14 | setProperty("org.jooq.no-logo", "true") 15 | setProperty("org.jooq.no-tips", "true") 16 | } 17 | 18 | runApplication(*args) 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/extension/json/JsonAssert.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.json 2 | 3 | import net.javacrumbs.jsonunit.assertj.JsonAssert 4 | import net.javacrumbs.jsonunit.assertj.JsonAssertions 5 | import org.springframework.test.web.reactive.server.WebTestClient 6 | 7 | /** 8 | * Created by kevin on 2019-02-19 9 | */ 10 | fun WebTestClient.BodyContentSpec.assertThatJson(t: JsonAssert.ConfigurableJsonAssert.() -> Unit ): WebTestClient.BodyContentSpec { 11 | val json = String(returnResult().responseBody!!) 12 | t(JsonAssertions.assertThatJson(json)) 13 | return this 14 | } 15 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/title/title.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 07/05/2016. 3 | */ 4 | import { Module, Component } from './../../../decorators'; 5 | import { TitleService } from './../../service/title.service'; 6 | 7 | @Module({ name : 'ps.common.component.title', modules : [ TitleService ] }) 8 | @Component({ selector : 'title', template : `{{ tc.title }}`, as: 'tc'}) 9 | export class TitleComponent { 10 | 11 | title = null; 12 | 13 | constructor(TitleService, $scope) { 14 | "ngInject"; 15 | TitleService.title.subscribe(newTitle => $scope.$evalAsync(() => this.title = newTitle)); 16 | } 17 | } -------------------------------------------------------------------------------- /frontend-angularjs/gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 01/11/2015 for Podcast Server 3 | */ 4 | 5 | import gulp from 'gulp'; 6 | import browserSync from 'browser-sync'; 7 | import paths from '../paths'; 8 | 9 | gulp.task('watch',['less', 'fonts', 'lint-js'], () => { 10 | gulp.watch(paths.glob.less, ['less', browserSync.reload ]); 11 | gulp.watch(paths.glob.js, ['lint-js', browserSync.reload ]); 12 | gulp.watch([paths.jspm.fonts, paths.glob.fonts], ['fonts', browserSync.reload ]); 13 | gulp.watch(paths.glob.html, browserSync.reload ); 14 | }); -------------------------------------------------------------------------------- /frontend-angular/src/app/search/search.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Item, Page, SearchItemPageRequest } from '../shared/entity'; 3 | 4 | export enum SearchAction { 5 | SEARCH = '[Items] Search', 6 | SEARCH_SUCCESS = '[Items] Search Success' 7 | } 8 | 9 | export class Search implements Action { 10 | readonly type = SearchAction.SEARCH; 11 | constructor(public pageRequest: SearchItemPageRequest) {} 12 | } 13 | 14 | export class SearchSuccess implements Action { 15 | readonly type = SearchAction.SEARCH_SUCCESS; 16 | 17 | constructor(public results: Page) {} 18 | } 19 | 20 | export type SearchActions = Search | SearchSuccess; 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/FindHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find 2 | 3 | import org.springframework.web.servlet.function.ServerRequest 4 | import org.springframework.web.servlet.function.ServerResponse 5 | import org.springframework.web.servlet.function.body 6 | import java.net.URI 7 | 8 | class FindHandler( 9 | private val finderService: FindService 10 | ) { 11 | 12 | fun find(r: ServerRequest): ServerResponse { 13 | val uri = r.body().let(URI::create) 14 | val podcastMetadata = finderService.find(uri) 15 | return ServerResponse.ok().body(podcastMetadata) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/FindService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find 2 | 3 | import com.github.davinkevin.podcastserver.find.finders.Finder 4 | import java.net.URI 5 | 6 | /** 7 | * Created by kevin on 2019-08-11 8 | */ 9 | class FindService( 10 | val finders: Set 11 | ) { 12 | fun find(url: URI): FindPodcastInformation { 13 | val finder = finders.minBy { it.compatibility(url.toASCIIString()) } 14 | 15 | return finder.findPodcastInformation(url.toASCIIString()) 16 | ?: FindPodcastInformation(title = "", url = url, type = "RSS", cover = null, description = "") 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/navbar/navbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 25/10/2015 for PodcastServer 3 | */ 4 | import {Component, Module} from '../../../decorators'; 5 | import template from './navbar.html!text'; 6 | import './navbar.css!'; 7 | 8 | @Module({ 9 | name : 'ps.common.component.navbar' 10 | }) 11 | @Component({ 12 | selector : 'navbar', 13 | as : 'navbar', 14 | replace : true, 15 | template : template, 16 | transclude : true 17 | }) 18 | export default class NavbarComponent { 19 | constructor($element) { 20 | "ngInject"; 21 | this.navCollapsed = true; 22 | $element.removeClass('hidden'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/watchlist-chooser/watchlist-chooser.less: -------------------------------------------------------------------------------- 1 | 2 | body.modal-open { 3 | .modal-backdrop { 4 | height: 1000%; 5 | } 6 | 7 | .modal-header, .modal-footer { 8 | border-bottom: none; 9 | border-top: none; 10 | } 11 | 12 | .modal-body { 13 | 14 | padding: 0; 15 | 16 | .list-group { 17 | 18 | margin-bottom: 0; 19 | 20 | .list-group-item { 21 | 22 | border-radius: 0; 23 | 24 | &.play-mode { 25 | cursor: pointer; 26 | } 27 | 28 | cursor: default !important; 29 | span.fa { 30 | cursor: pointer; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/podcasts/details/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | - 5 | 6 | + 7 |
8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/dailymotion/karimdebbache.ids.10.items.json: -------------------------------------------------------------------------------- 1 | { 2 | "page":1, 3 | "limit":10, 4 | "explicit":false, 5 | "total":44, 6 | "has_more":true, 7 | "list":[ 8 | { 9 | "id":"x5ikng3" 10 | }, 11 | { 12 | "id":"x5a365y" 13 | }, 14 | { 15 | "id":"x54m781" 16 | }, 17 | { 18 | "id":"x4yzane" 19 | }, 20 | { 21 | "id":"x4sxtow" 22 | }, 23 | { 24 | "id":"x4l7o3v" 25 | }, 26 | { 27 | "id":"x4eq3z9" 28 | }, 29 | { 30 | "id":"x458bdl" 31 | }, 32 | { 33 | "id":"x3xg0nh" 34 | }, 35 | { 36 | "id":"x3r6gba" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/config/TomcatConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.config 2 | 3 | import org.apache.coyote.ProtocolHandler 4 | import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import java.util.concurrent.Executors 8 | 9 | @Configuration 10 | class TomcatConfig { 11 | 12 | @Bean 13 | fun protocolHandlerVirtualThreadExecutorCustomizer() = TomcatProtocolHandlerCustomizer { 14 | proto: ProtocolHandler -> proto.executor = Executors.newVirtualThreadPerTaskExecutor() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/FindersExtension.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders 2 | 3 | import com.github.davinkevin.podcastserver.find.FindCoverInformation 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import org.jsoup.nodes.Document 6 | import java.net.URI 7 | 8 | internal fun ImageService.fetchFindCoverInformation(url: URI): FindCoverInformation? { 9 | val info = fetchCoverInformation(url) 10 | ?: return null 11 | 12 | return FindCoverInformation(info.height, info.width, info.url) 13 | } 14 | 15 | internal fun Document.meta(s: String) = this.select("meta[$s]").attr("content") -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/CoverExists.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.storage 2 | 3 | import java.nio.file.Path 4 | import java.util.* 5 | 6 | interface CoverExists { 7 | fun coverExists(r: CoverExistsRequest): Path? 8 | } 9 | 10 | sealed class CoverExistsRequest { 11 | data class ForPlaylist (val name: String, val id: UUID, val coverExtension: String): CoverExistsRequest() 12 | data class ForPodcast (val title: String, val id: UUID, val coverExtension: String): CoverExistsRequest() 13 | data class ForItem (val podcastTitle: String, val id: UUID, val coverExtension: String): CoverExistsRequest() 14 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/messaging/MessagingConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.messaging 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Import 6 | import org.springframework.web.servlet.function.router 7 | 8 | 9 | /** 10 | * Created by kevin on 01/05/2020 11 | */ 12 | @Configuration 13 | @Import(MessageHandler::class, MessagingTemplate::class) 14 | class MessagingRoutingConfig { 15 | 16 | @Bean 17 | fun messageRouter(message: MessageHandler) = router { 18 | GET("/api/v1/sse", message::sseMessages) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/utils/custom/ffmpeg/CustomRunProcessFuncTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils.custom.ffmpeg 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | class CustomRunProcessFuncTest { 7 | 8 | @Test 9 | fun `should get process from listener`() { 10 | /* Given */ 11 | val cp = CustomRunProcessFunc() 12 | val pl = ProcessListener("anUrl") 13 | 14 | /* When */ 15 | val p = (cp + pl) 16 | .run(listOf("/bin/bash", "anUrl", "Foo", "Bar")) 17 | 18 | /* Then */ 19 | assertThat(p).isSameAs(pl.process.get()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/youtube/joueurdugrenier.channel.with-0-item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | yt:playlist:PLAD454F0807B6CB80 5 | PLAD454F0807B6CB80 6 | UC_yP2DpIgs5Y1uWC0T03Chw 7 | Tests du grenier 8 | 9 | joueurdugrenier 10 | http://www.youtube.com/channel/UC_yP2DpIgs5Y1uWC0T03Chw 11 | 12 | 2010-09-20T16:29:36+00:00 13 | 14 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/ToExternalUrl.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.storage 2 | 3 | import java.net.URI 4 | import java.nio.file.Path 5 | 6 | interface ToExternalUrl { 7 | fun toExternalUrl(r: ExternalUrlRequest): URI 8 | } 9 | 10 | sealed class ExternalUrlRequest(open val host: URI) { 11 | data class ForPlaylist(override val host: URI, val playlistName: String, val file: Path): ExternalUrlRequest(host) 12 | data class ForPodcast(override val host: URI, val podcastTitle: String, val file: Path): ExternalUrlRequest(host) 13 | data class ForItem(override val host: URI, val podcastTitle: String, val file: Path): ExternalUrlRequest(host) 14 | } -------------------------------------------------------------------------------- /backend/src/test/resources/remote/podcast/youtube/joueurdugrenier.playlist.with-0-item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | yt:playlist:PLAD454F0807B6CB80 5 | PLAD454F0807B6CB80 6 | UC_yP2DpIgs5Y1uWC0T03Chw 7 | Tests du grenier 8 | 9 | Joueur Du Grenier 10 | https://www.youtube.com/channel/UC_yP2DpIgs5Y1uWC0T03Chw 11 | 12 | 2010-09-20T16:29:36+00:00 13 | 14 | -------------------------------------------------------------------------------- /frontend-angular/src/app/item/item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemComponent } from './item.component'; 4 | 5 | xdescribe('ItemComponent', () => { 6 | let component: ItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | async(() => { 11 | TestBed.configureTestingModule({ declarations: [ItemComponent] }).compileComponents(); 12 | }) 13 | ); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ItemComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/app.js: -------------------------------------------------------------------------------- 1 | import {Boot, Module} from './decorators'; 2 | import { TitleComponent } from './common/component/title/title'; 3 | import SearchModule from './search/search'; 4 | import PodcastsModule from './podcasts/podcasts'; 5 | import ItemModule from './item/item'; 6 | import DownloadModule from './download/download'; 7 | import PlayerModule from './player/player'; 8 | import StatsModule from './stats/stats'; 9 | import ConfigModule from './config/config'; 10 | 11 | @Boot({ element : document }) 12 | @Module({ 13 | name : 'podcastApp', 14 | modules : [ TitleComponent, SearchModule, PodcastsModule, ItemModule, DownloadModule, PlayerModule, StatsModule, ConfigModule ] 15 | }) 16 | export default class App {} 17 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/migrations/V9__rename_watchlist_to_playlist.sql: -------------------------------------------------------------------------------- 1 | alter table watch_list rename constraint watch_list_pkey to playlist_pkey; 2 | alter table watch_list rename constraint watch_list_name_key to playlist_name_key; 3 | alter table watch_list rename to playlist; 4 | 5 | alter table watch_list_items rename column watch_lists_id to playlists_id; 6 | alter table watch_list_items rename constraint watch_list_items_pkey to playlist_items_pkey; 7 | alter table watch_list_items rename constraint watch_list_items_items_id_fkey to playlist_items_items_id_fkey; 8 | alter table watch_list_items rename constraint watch_list_items_watch_lists_id_fkey to playlist_items_playlists_id_fkey; 9 | alter table watch_list_items rename to playlist_items; 10 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/mapper/HttpHeader.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl.mapper; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public class HttpHeader { 8 | 9 | @JsonProperty("Accept-Charset") 10 | public String acceptCharset; 11 | @JsonProperty("Accept-Language") 12 | public String acceptLanguage; 13 | @JsonProperty("Accept-Encoding") 14 | public String acceptEncoding; 15 | @JsonProperty("Accept") 16 | public String accept; 17 | @JsonProperty("User-Agent") 18 | public String userAgent; 19 | } 20 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/podcasts/podcasts.less: -------------------------------------------------------------------------------- 1 | .ps { 2 | .podcast-list { 3 | 4 | .search-bar { 5 | margin: 16px 0; 6 | 7 | .form-group { 8 | margin-right: 16px 9 | } 10 | } 11 | 12 | *[class^=col-] { 13 | padding-right: 8px; 14 | padding-left: 8px; 15 | } 16 | 17 | * .thumb { 18 | margin-bottom: 15px; 19 | } 20 | img { 21 | margin: 0 auto; 22 | position: relative; 23 | border-radius: 5px; 24 | background-color: #fff; 25 | box-shadow: 0 2px 6px 2px rgba(66, 66, 66, 0.75); 26 | height: 200px; 27 | object-fit: cover; 28 | &:hover { 29 | box-shadow: 0 2px 6px 2px rgba(51, 122, 183, 0.50); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.19.0 2 | 3 | FROM flyway/flyway:11.13.3 AS flyway-base 4 | 5 | RUN echo "Remove useless drivers for additional databases" && \ 6 | find /flyway/drivers -iname *.jar | grep -v postgresql | grep -v jackson | xargs rm && \ 7 | rm -rf /flyway/licenses/ /flyway/README.txt /flyway/flyway* /flyway/drivers/*.txt /flyway/conf/ /flyway/assets 8 | 9 | COPY backend-lib-database/src/main/migrations/*.sql /flyway/sql/ 10 | 11 | FROM gcr.io/distroless/java21-debian12:latest as base-image 12 | 13 | COPY --from=flyway-base /flyway/ /database/ 14 | WORKDIR /database/ 15 | 16 | ENTRYPOINT [ "java", "-cp", "lib/*:lib/flyway/*:lib/aad/*:drivers/*", "org.flywaydb.commandline.Main", "migrate"] 17 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/IOUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | import java.nio.file.Paths 6 | 7 | /** 8 | * Created by kevin on 23/07/2016. 9 | */ 10 | private object IOUtils {} 11 | 12 | fun toPath(uri: String): Path { 13 | val file = IOUtils::class.java.getResource(uri)?.toURI() ?: error("file $uri not found") 14 | return Paths.get(file) 15 | } 16 | 17 | fun fileAsString(uri: String): String { 18 | return Files.newInputStream(toPath(uri)) 19 | .bufferedReader() 20 | .use { it.readText() } 21 | } 22 | 23 | fun fileAsByteArray(uri: String): ByteArray { 24 | return Files.readAllBytes(toPath(uri)) 25 | } 26 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/backend/remote-debug/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | patches: 5 | - patch: |- 6 | apiVersion: apps/v1 7 | kind: Deployment 8 | metadata: 9 | name: backend 10 | spec: 11 | template: 12 | spec: 13 | containers: 14 | - name: app 15 | image: podcastserver/backend 16 | env: 17 | - name: JAVA_TOOL_OPTIONS 18 | value: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" 19 | ports: 20 | - name: remote-debug 21 | containerPort: 5005 22 | protocol: TCP 23 | -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/ingress/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: podcast-server 5 | spec: 6 | tls: 7 | - hosts: 8 | - localhost # {"$kpt-set":"domain"} 9 | secretName: podcast-server-tls 10 | rules: 11 | - host: localhost # {"$kpt-set":"domain"} 12 | http: 13 | paths: 14 | - path: /api 15 | backend: 16 | serviceName: backend 17 | servicePort: 8080 18 | - path: /actuator 19 | backend: 20 | serviceName: backend 21 | servicePort: 8080 22 | - path: /data 23 | backend: 24 | serviceName: fs 25 | servicePort: 80 26 | - backend: 27 | serviceName: ui 28 | servicePort: 80 29 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToolbarComponent } from './toolbar.component'; 4 | 5 | xdescribe('ToolbarComponent', () => { 6 | let component: ToolbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ToolbarComponent] 13 | }).compileComponents(); 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(ToolbarComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/kodi/KodiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.kodi 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Import 6 | import org.springframework.web.servlet.function.router 7 | 8 | @Configuration 9 | @Import(KodiHandler::class) 10 | class KodiRouterConfig { 11 | 12 | @Bean 13 | fun kodiRouter(handler: KodiHandler) = router { 14 | GET("/kodi/", handler::podcasts) 15 | GET("/kodi/{podcastName}/", handler::items) 16 | GET("/kodi/{podcastName}/{itemTitle}", handler::item) 17 | } 18 | 19 | } 20 | 21 | @Configuration 22 | @Import(KodiRepository::class) 23 | class KodiConfig -------------------------------------------------------------------------------- /frontend-angularjs/gulp/tasks/maven.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 15/04/2016. 3 | */ 4 | import fs from 'fs'; 5 | import jsxml from 'node-jsxml'; 6 | import paths from '../paths'; 7 | 8 | jsxml.XML.setSettings({ignoreComments : false, ignoreProcessingInstructions : false, createMainDocument: true}); 9 | 10 | export default function update(version) { 11 | var xmlDoc = new jsxml.XML(fs.readFileSync(paths.pomXml, 'utf8')); 12 | var node = xmlDoc.child('project').child('version'); 13 | if (node != "" && node.getValue() != version) { 14 | node.setValue(version); 15 | // Replace is for removing a bug in toXmlString when used from gulp... don't know why !?!? 16 | fs.writeFileSync(paths.pomXml, xmlDoc.toXMLString().replace(/ ="undefined"/g, "")); 17 | } 18 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/youtube/YoutubeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.youtube 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.Type 4 | import java.net.URI 5 | 6 | internal fun isPlaylist(url: URI) = url.toASCIIString().contains("playlist?list=") 7 | internal fun isHandle(url: URI): Boolean { 8 | return url.path.matches("^/@[^/]*$".toRegex()) || url.path.startsWith("/c/") 9 | } 10 | internal fun isChannel(url: URI) = url.toASCIIString().matches(".*/channel/UC.*".toRegex()) 11 | 12 | internal val type = Type("Youtube", "Youtube") 13 | internal fun youtubeCompatibility(url: String) = when { 14 | "youtube.com/" in url -> 1 15 | else -> Integer.MAX_VALUE 16 | } 17 | 18 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/Downloader.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders 2 | 3 | import com.github.davinkevin.podcastserver.download.ItemDownloadManager 4 | 5 | 6 | interface Downloader: Runnable { 7 | 8 | val downloadingInformation: DownloadingInformation 9 | 10 | fun download(): DownloadingItem 11 | 12 | override fun run() = startDownload() 13 | fun startDownload() 14 | fun stopDownload() 15 | fun failDownload() 16 | fun finishDownload() 17 | } 18 | 19 | interface DownloaderFactory { 20 | fun with(information: DownloadingInformation, itemDownloadManager: ItemDownloadManager): Downloader 21 | fun compatibility(downloadingInformation: DownloadingInformation): Int 22 | } 23 | 24 | -------------------------------------------------------------------------------- /frontend-angular/src/app/floating-player/floating-player.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatIconModule, MatToolbarModule } from '@angular/material'; 4 | import { FloatingPlayerComponent } from '#app/floating-player/floating-player.component'; 5 | import { floatingPlayer } from '#app/floating-player/floating-player.reducer'; 6 | import { StoreModule } from '@ngrx/store'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | /* Core */ CommonModule, 11 | /* Material */ MatToolbarModule, 12 | MatIconModule, 13 | /* NgRx */ StoreModule.forFeature('floatingPlayer', floatingPlayer) 14 | ], 15 | exports: [FloatingPlayerComponent], 16 | declarations: [FloatingPlayerComponent] 17 | }) 18 | export class FloatingPlayerModule {} 19 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/core/episodes/episodes.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 |

{{ item.title }}

14 |

{{ item.pubDate | date: 'dd/MM/yyyy à HH:mm'}}

15 |
16 |
17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Global : 2 | .idea 3 | !.idea/icon.png 4 | *.iml 5 | *.DS_Store 6 | node_modules 7 | logs 8 | target 9 | .gradle 10 | build 11 | yarn-error.log 12 | 13 | # Front End in AngularJS : 14 | frontend-angularjs/www/fonts/ 15 | frontend-angularjs/www/jspm_packages 16 | frontend-angularjs/node 17 | frontend-angularjs/**/*.css 18 | 19 | # Front End in Angular : 20 | frontend-angular/dist 21 | frontend-angular/coverage 22 | 23 | # Back End 24 | Backend/lib/ 25 | Backend/src/main/**/application.yml 26 | Backend/build 27 | Backend/lan.dk.podcastserver.* 28 | Backend/etc 29 | backend-lib-youtubedl/build 30 | backend-lib-database/build/ 31 | 32 | /lan.dk.podcastserver.entity.Item/ 33 | /backend/buildSrc/build/ 34 | /backend/buildSrc/gradle/wrapper/ 35 | /documentation/dist/ 36 | /gradle.properties 37 | /.env.local 38 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/entity/StatusTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.entity 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.assertj.core.api.Assertions.assertThatThrownBy 5 | import org.junit.jupiter.api.Test 6 | 7 | /** 8 | * Created by kevin on 14/06/15 for HackerRank problem 9 | */ 10 | class StatusTest { 11 | 12 | @Test 13 | fun `should check value`() { 14 | assertThat(Status.of("NOT_DOWNLOADED")) 15 | .isEqualTo(Status.NOT_DOWNLOADED) 16 | } 17 | 18 | @Test 19 | fun `should throw exception`() { 20 | assertThatThrownBy { Status.of("") } 21 | .isInstanceOf(IllegalArgumentException::class.java) 22 | .hasMessage("No enum constant Status.") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/service/podcast/podcast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Podcast } from '../../entity'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Injectable() 8 | export class PodcastService { 9 | constructor(private http: HttpClient) {} 10 | 11 | findAll(): Observable { 12 | return this.http.get<{ content: Podcast[] }>('/api/v1/podcasts').pipe( 13 | map(v => v.content) 14 | ); 15 | } 16 | 17 | findById(id: string): Observable { 18 | return this.http.get(`/api/v1/podcasts/${id}`); 19 | } 20 | 21 | refresh(p: Podcast): Observable { 22 | return this.http.get(`/api/podcasts/${p.id}/update/force`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/type/TypeConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.podcast.type 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.UpdaterSelector 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.context.annotation.Import 7 | import org.springframework.web.servlet.function.router 8 | 9 | @Configuration 10 | @Import(TypeHandler::class) 11 | class TypeRoutingConfig { 12 | 13 | @Bean 14 | fun typeRouter(type: TypeHandler) = router { 15 | GET("/api/v1/podcasts/types", type::findAll) 16 | } 17 | } 18 | 19 | @Configuration 20 | @Import( 21 | TypeRoutingConfig::class, 22 | UpdaterSelector::class 23 | ) 24 | class TypeConfig 25 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/core/episodes/episodes.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../variables"; 2 | 3 | a { 4 | text-decoration: none; 5 | } 6 | 7 | .mat-list-item { 8 | &.mat-list-item-avatar { 9 | height: 100px; 10 | padding-bottom: 8px; 11 | 12 | img { 13 | object-fit: cover; 14 | } 15 | 16 | .mat-list-avatar { 17 | height: 100px; 18 | width: 100px; 19 | border-radius: 0; 20 | } 21 | 22 | h3.mat-line { 23 | font-family: inherit; 24 | font-weight: 500; 25 | line-height: 19.8px; 26 | color: inherit; 27 | font-size: 18px; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | } 32 | 33 | } 34 | 35 | } 36 | 37 | mat-paginator { 38 | background-color: $global-background-color; 39 | } 40 | -------------------------------------------------------------------------------- /frontend-angular/src/app/search/resolver/search-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SearchItemPageRequest } from '#app/shared/entity'; 3 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { select, Store } from '@ngrx/store'; 6 | import { searchRequest } from '../search.reducer'; 7 | import { take } from 'rxjs/operators'; 8 | import { AppState } from '#app/app.reducer'; 9 | 10 | @Injectable() 11 | export class SearchQueryResolver implements Resolve { 12 | constructor(private store: Store) {} 13 | 14 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 15 | return this.store.pipe(select(searchRequest), take(1)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/config/BeanConfigScanTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.config 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import java.nio.file.Paths 6 | 7 | /** 8 | * Created by kevin on 13/08/15 for Podcast Server 9 | */ 10 | class BeanConfigScanTest { 11 | 12 | private val beanConfigScan: BeanConfigScan = BeanConfigScan() 13 | 14 | @Test 15 | fun should_provide_a_converter_from_string_to_path() { 16 | /* Given */ 17 | val c = beanConfigScan.pathConverter() 18 | val path = "/tmp" 19 | 20 | /* When */ 21 | val convertedPath = c.convert(path) 22 | 23 | /* Then */ 24 | assertThat(convertedPath) 25 | .isEqualTo(Paths.get(path)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/utils/StreamGobbler.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl.utils; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | public class StreamGobbler extends Thread { 7 | 8 | private InputStream stream; 9 | private StringBuffer buffer; 10 | 11 | public StreamGobbler(StringBuffer buffer, InputStream stream) { 12 | this.stream = stream; 13 | this.buffer = buffer; 14 | start(); 15 | } 16 | 17 | public void run() { 18 | try { 19 | int nextChar; 20 | while((nextChar = this.stream.read()) != -1) { 21 | this.buffer.append((char) nextChar); 22 | } 23 | } 24 | catch (IOException e) { 25 | 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/utils/MatcherExtractor.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils 2 | 3 | import java.util.regex.Matcher 4 | import java.util.regex.Pattern 5 | 6 | /** 7 | * Created by kevin on 09/07/2018 8 | */ 9 | class MatcherExtractor(private val matcher: Matcher, private val isFind: Boolean) { 10 | 11 | constructor(m: Matcher) : this(m, m.find()) 12 | 13 | companion object { 14 | @JvmStatic fun from(p: Pattern) = PatternExtractor(p) 15 | fun from(s: String) = PatternExtractor(s.toPattern()) 16 | } 17 | 18 | fun group(i: Int): String? = when { 19 | !isFind -> null 20 | else -> matcher.group(i) 21 | } 22 | 23 | class PatternExtractor(private val p: Pattern) { 24 | fun on(v: String) = MatcherExtractor(p.matcher(v)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/docker-for-desktop/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: podcast-server 5 | 6 | resources: 7 | - ../all-in-one 8 | 9 | components: 10 | - ../../base/components/namespace 11 | - ingress 12 | 13 | patchesStrategicMerge: 14 | - volume.yaml 15 | 16 | configMapGenerator: 17 | - name: storage 18 | behavior: merge 19 | literals: 20 | - bucket=dev-podcasts 21 | 22 | secretGenerator: 23 | - name: database 24 | behavior: merge 25 | literals: 26 | - password=nAAdo5wNs7WEF1UxUobpJDfS9Si62PHa 27 | - name: podcast-server 28 | behavior: merge 29 | literals: 30 | - api.youtube=TO_BE_DEFINED 31 | type: Opaque 32 | - name: storage 33 | behavior: merge 34 | literals: 35 | - password=Mns1G6RgPtLgy68H 36 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/examples/installation/kubernetes/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: podcast-server # <1> 5 | 6 | bases: 7 | - git::https://gitlab.com/davinkevin/Podcast-Server.git//deployment/kubernetes/base/ 8 | 9 | resources: 10 | - ingress.yaml # <2> 11 | 12 | patchesStrategicMerge: 13 | - backend.yaml # <3> 14 | - database.yaml # <4> 15 | - fs.yaml # <5> 16 | 17 | configMapGenerator: 18 | - files: 19 | - application.yaml # <6> 20 | literals: 21 | - database.username=podcast-server-user # <7> 22 | - database.url=jdbc:postgresql://database:5432/podcast-server # <8> 23 | name: podcast-server 24 | 25 | secretGenerator: 26 | - name: podcast-server 27 | literals: 28 | - api.youtube=foo # <9> 29 | - database.password=bar # <10> 30 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {select, Store} from '@ngrx/store'; 3 | import {AppState, selectSideNavOpen} from './app.reducer'; 4 | import {CloseSideNavAction} from './app.actions'; 5 | 6 | @Component({ 7 | selector: 'ps-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent implements OnInit { 12 | 13 | sideNavOpen = false; 14 | 15 | constructor(private store: Store) {} 16 | 17 | ngOnInit(): void { 18 | this.store.pipe( 19 | select(selectSideNavOpen) 20 | ).subscribe(v => this.sideNavOpen = v); 21 | } 22 | 23 | onOpenChange($event: boolean) { 24 | if ($event === true) { 25 | return; 26 | } 27 | 28 | this.store.dispatch(new CloseSideNavAction()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { Podcast } from '../shared/entity'; 4 | 5 | import { PodcastsActions, PodcastsAction } from './podcasts.actions'; 6 | 7 | export interface PodcastsState { 8 | podcasts: Podcast[]; 9 | } 10 | 11 | const initialState: PodcastsState = { 12 | podcasts: [] 13 | }; 14 | 15 | export function reducer(state = initialState, action: PodcastsActions): PodcastsState { 16 | switch (action.type) { 17 | case PodcastsAction.FIND_ALL_SUCCESS: { 18 | return { ...state, podcasts: action.podcasts }; 19 | } 20 | 21 | default: { 22 | return state; 23 | } 24 | } 25 | } 26 | 27 | const moduleSelector = createFeatureSelector('podcasts'); 28 | export const podcasts = createSelector(moduleSelector, (s: PodcastsState) => s.podcasts); 29 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/mapper/VideoFormat.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl.mapper; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public class VideoFormat { 8 | 9 | public int asr; 10 | public int tbr; 11 | public int abr; 12 | public String format; 13 | @JsonProperty("format_id") 14 | public String formatId; 15 | @JsonProperty("format_note") 16 | public String formatNote; 17 | public String ext; 18 | public int preference; 19 | public String vcodec; 20 | public String acodec; 21 | public int width; 22 | public int height; 23 | public long filesize; 24 | public int fps; 25 | public String url; 26 | } 27 | -------------------------------------------------------------------------------- /frontend-angular/src/app/search/search.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | .search__bar { 4 | display: flex; 5 | flex-flow: row wrap; 6 | align-items: center; 7 | justify-content: space-around; 8 | min-height: 7vh; 9 | 10 | .search__sort { 11 | display: flex; 12 | flex-flow: row wrap; 13 | align-items: center; 14 | justify-content: space-around; 15 | } 16 | } 17 | 18 | mat-paginator { 19 | background-color: $global-background-color; 20 | } 21 | 22 | .search__results { 23 | list-style: none; 24 | display: flex; 25 | 26 | flex-flow: row wrap; 27 | justify-content: space-around; 28 | 29 | mat-card { 30 | width: 215px; 31 | margin: 5px; 32 | 33 | [mat-card-image] { 34 | height: 263px; 35 | object-fit: cover; 36 | &:first-child { 37 | margin-top: 0; 38 | } 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/DeleteObject.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.storage 2 | 3 | import java.nio.file.Path 4 | import java.nio.file.Paths 5 | import java.util.* 6 | 7 | interface DeleteObject { 8 | fun delete(request: DeleteRequest): Boolean 9 | } 10 | 11 | sealed class DeleteRequest { 12 | data class ForPodcast(val id: UUID, val title: String): DeleteRequest() 13 | data class ForItem(val id: UUID, val fileName: Path, val podcastTitle: String): DeleteRequest() { 14 | val path: Path = Paths.get(podcastTitle).resolve(fileName) 15 | } 16 | data class ForCover(val id: UUID, val extension: String, val item: Item, val podcast: Podcast): DeleteRequest() { 17 | data class Podcast(val id: UUID, val title: String) 18 | data class Item(val id: UUID, val title: String) 19 | } 20 | } -------------------------------------------------------------------------------- /frontend-angular/src/app/floating-player/floating-player.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position:fixed; 3 | bottom:0; 4 | right:0; 5 | z-index: 999; 6 | background-color: white; 7 | max-width: 512px; 8 | max-height: 512px; 9 | 10 | ::ng-deep .mat-toolbar-single-row { 11 | height: 48px; 12 | } 13 | 14 | box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); 15 | padding-bottom: -4px; 16 | } 17 | 18 | 19 | mat-icon { 20 | padding: 0 14px; 21 | } 22 | 23 | .spacer { 24 | flex: 1 1 auto; 25 | } 26 | 27 | .player-wrapper { 28 | background-color: black; 29 | max-width: 512px; 30 | } 31 | 32 | .audio-player { 33 | img { 34 | object-fit: cover; 35 | width: 100%; 36 | max-height: 448px; 37 | } 38 | } 39 | 40 | video { 41 | max-width: 512px; 42 | max-height: 512px; 43 | margin-bottom: -4px; 44 | /*max-height: 448px;*/ 45 | } 46 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { Action } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | 7 | import { Podcast } from '../shared/entity'; 8 | import { PodcastService } from '../shared/service/podcast/podcast.service'; 9 | 10 | import { FindAllSuccess, PodcastsAction } from './podcasts.actions'; 11 | 12 | @Injectable() 13 | export class PodcastsEffects { 14 | @Effect() 15 | findAll$: Observable = this.actions$.pipe( 16 | ofType(PodcastsAction.FIND_ALL), 17 | switchMap(() => this.podcastService.findAll()), 18 | map((results: Podcast[]) => new FindAllSuccess(results)) 19 | ); 20 | 21 | constructor(private actions$: Actions, private podcastService: PodcastService) {} 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/tag/TagConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.tag 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.context.annotation.Import 6 | import org.springframework.web.servlet.function.router 7 | 8 | /** 9 | * Created by kevin on 2019-03-19 10 | */ 11 | @Configuration 12 | @Import(TagHandler::class) 13 | class TagRoutingConfig { 14 | @Bean 15 | fun tagRouter(tag: TagHandler) = router { 16 | "/api/v1/tags".nest { 17 | GET("/search", tag::findByNameLike) 18 | GET("/{id}", tag::findById) 19 | } 20 | } 21 | } 22 | 23 | @Configuration 24 | @Import( 25 | TagRepository::class, 26 | TagRoutingConfig::class, 27 | TagService::class, 28 | ) 29 | class TagConfig 30 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/copy/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 12/12/2015 for Podcast Server 3 | */ 4 | 5 | import {Directive, Module} from '../../../decorators'; 6 | import Clipboard from 'clipboard'; 7 | 8 | @Module({ 9 | name : 'ps.common.component.copy' 10 | }) 11 | @Directive({ 12 | selector : 'copy', 13 | bindToController : { copy: '@'}, 14 | as : 'c' 15 | }) 16 | export default class Copy { 17 | 18 | constructor($window) { 19 | "ngInject"; 20 | this.baseUrl = $window.location.origin; 21 | } 22 | 23 | get url() { 24 | return this.copy.substring(0, 1) === '/' ? this.baseUrl + this.copy : this.copy; 25 | } 26 | 27 | static link(scope, element, _, ctrl) { 28 | let clipboard = new Clipboard(element[0], { text: () => ctrl.url}); 29 | scope.$on('destroy', () => clipboard.destroy()); 30 | } 31 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.cover 2 | 3 | import org.springframework.web.servlet.function.ServerRequest 4 | import org.springframework.web.servlet.function.ServerResponse 5 | import org.springframework.web.servlet.function.paramOrNull 6 | import java.time.Clock 7 | import java.time.OffsetDateTime 8 | 9 | class CoverHandler( 10 | private val cover: CoverService, 11 | private val clock: Clock 12 | ) { 13 | 14 | fun deleteOldCovers(r: ServerRequest): ServerResponse { 15 | val retentionNumberOfDays = r.paramOrNull("days")?.toLong() ?: 365L 16 | 17 | val date = OffsetDateTime.now(clock) 18 | .minusDays(retentionNumberOfDays) 19 | 20 | cover.deleteCoversInFileSystemOlderThan(date) 21 | 22 | return ServerResponse.ok().build() 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/type/TypeHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.podcast.type 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.UpdaterSelector 4 | import org.springframework.web.servlet.function.ServerRequest 5 | import org.springframework.web.servlet.function.ServerResponse 6 | 7 | class TypeHandler(updaterSelector: UpdaterSelector) { 8 | 9 | private val types by lazy { 10 | updaterSelector.types() 11 | .map { TypeResponse.TypeHAL(it.key, it.name) } 12 | .let(::TypeResponse) 13 | } 14 | 15 | fun findAll(@Suppress("UNUSED_PARAMETER") r: ServerRequest): ServerResponse = 16 | ServerResponse.ok().body(types) 17 | } 18 | 19 | private class TypeResponse(@Suppress("unused") val content: Collection) { 20 | data class TypeHAL(val key: String, val name: String) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/podcast.k8s.local/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: podcast-server 5 | 6 | resources: 7 | - ../all-in-one 8 | 9 | components: 10 | - ../../base/components/namespace 11 | - ../../base/components/backend/remote-debug 12 | - gateways 13 | 14 | patchesStrategicMerge: 15 | - volume.yaml 16 | 17 | configMapGenerator: 18 | - name: storage 19 | behavior: merge 20 | literals: 21 | - bucket=dev-podcasts 22 | 23 | secretGenerator: 24 | - name: database 25 | behavior: merge 26 | literals: 27 | - password=nAAdo5wNs7WEF1UxUobpJDfS9Si62PHa 28 | - name: podcast-server 29 | behavior: merge 30 | literals: 31 | - api.youtube=TO_BE_DEFINED 32 | type: Opaque 33 | - name: storage 34 | behavior: merge 35 | literals: 36 | - password=Mns1G6RgPtLgy68H 37 | -------------------------------------------------------------------------------- /frontend-angular/src/app/item/core/item.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | import { select, Store } from '@ngrx/store'; 4 | import { Injectable } from '@angular/core'; 5 | import { skip, take } from 'rxjs/operators'; 6 | import { Item } from '#app/shared/entity'; 7 | import { AppState } from '#app/app.reducer'; 8 | import { FindOneAction } from '../item.actions'; 9 | import { item } from '../item.reducer'; 10 | 11 | @Injectable() 12 | export class ItemResolver implements Resolve { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | this.store.dispatch(new FindOneAction(route.params.id, route.params.podcastId)); 17 | 18 | return this.store.pipe(select(item), skip(1), take(1)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/ffmpeg/FfmpegConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.ffmpeg 2 | 3 | import com.github.davinkevin.podcastserver.service.properties.ExternalTools 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | 7 | /** 8 | * Created by kevin on 13/06/2016 for Podcast Server 9 | */ 10 | class FfmpegConfigTest { 11 | 12 | private val ffmpegConfig = FfmpegConfig() 13 | 14 | @Test 15 | fun `should have a bean for executor`() { 16 | /* Given */ 17 | val externalTools = ExternalTools( 18 | ffmpeg = "/bin/echo", 19 | ffprobe = "/bin/echo" 20 | ) 21 | 22 | /* When */ 23 | val service = ffmpegConfig.ffmpegService(externalTools) 24 | 25 | /* Then */ 26 | assertThat(service).isNotNull() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend-angular/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.cover 2 | 3 | import com.github.davinkevin.podcastserver.config.ClockConfig 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.context.annotation.Import 7 | import org.springframework.web.servlet.function.router 8 | 9 | @Configuration 10 | @Import(CoverHandler::class) 11 | class CoverRoutingConfig { 12 | 13 | @Bean 14 | fun coverRouter(cover: CoverHandler) = router { 15 | "/api/v1/covers".nest { 16 | DELETE("", cover::deleteOldCovers) 17 | } 18 | } 19 | } 20 | 21 | @Configuration 22 | @Import( 23 | CoverRepository::class, 24 | CoverService::class, 25 | ClockConfig::class, 26 | CoverRoutingConfig::class 27 | ) 28 | class CoverConfig 29 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/core/resolver/podcasts.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { Podcast } from '#app/shared/entity'; 6 | import { podcasts } from '../../podcasts.reducer'; 7 | import * as PodcastsActions from '../../podcasts.actions'; 8 | import { skip, take } from 'rxjs/operators'; 9 | import { AppState } from '#app/app.reducer'; 10 | 11 | @Injectable() 12 | export class PodcastsResolver implements Resolve { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | this.store.dispatch(new PodcastsActions.FindAll()); 17 | 18 | return this.store.pipe(select(podcasts), skip(1), take(1)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/properties/PodcastServerParameters.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.properties 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import java.time.OffsetDateTime 5 | import java.time.ZonedDateTime.now 6 | 7 | /** 8 | * Created by kevin on 03/02/15. 9 | */ 10 | @ConfigurationProperties(value = "podcastserver") 11 | data class PodcastServerParameters( 12 | val maxUpdateParallels: Int = 256, 13 | val concurrentDownload: Int = 3, 14 | val numberOfTry: Int = 10, 15 | val numberOfDayToDownload: Long = 30L, 16 | val numberOfDayToSaveCover: Long = 365L, 17 | val rssDefaultNumberItem: Long = 50L 18 | ) { 19 | fun limitDownloadDate() = OffsetDateTime.now().minusDays(numberOfDayToDownload)!! 20 | fun limitToKeepCoverOnDisk() = now().minusDays(numberOfDayToSaveCover)!! 21 | } 22 | -------------------------------------------------------------------------------- /frontend-angular/src/app/item/core/podcast.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 4 | import { AppState } from '#app/app.reducer'; 5 | import { Observable } from 'rxjs'; 6 | import { skip, take } from 'rxjs/operators'; 7 | import { Podcast } from '#app/shared/entity'; 8 | import { FindParentPodcastAction } from '#app/item/item.actions'; 9 | import { podcast } from '#app/item/item.reducer'; 10 | 11 | @Injectable() 12 | export class PodcastResolver implements Resolve { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | this.store.dispatch(new FindParentPodcastAction(route.params.podcastId)); 17 | return this.store.pipe(select(podcast), skip(1), take(1)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/route.js: -------------------------------------------------------------------------------- 1 | import {Module, Config, Run} from '../decorators'; 2 | import 'angular-route'; 3 | import 'angular-hotkeys'; 4 | 5 | let registerGlobalHotkeys = ($location, hotkeys) => { 6 | "ngInject"; 7 | 8 | [ 9 | ['h', 'Goto Home', '/items'], 10 | ['p', 'Goto Podcast List','/podcasts'], 11 | ['d', 'Goto Download List', '/download'] 12 | ] 13 | .map(hotkey => ({ combo: hotkey[0], description: hotkey[1], callback: () => $location.path(hotkey[2]) })) 14 | .forEach(hotkey => hotkeys.add(hotkey)); 15 | }; 16 | 17 | @Module({ 18 | name : 'ps.config.route', 19 | modules : [ 'ngRoute', 'cfp.hotkeys'] 20 | }) 21 | @Run(registerGlobalHotkeys) 22 | @Config($routeProvider => {"ngInject"; return $routeProvider.otherwise({redirectTo: '/items'});}) 23 | @Config($locationProvider => {"ngInject"; return $locationProvider.html5Mode(true);}) 24 | export default class RouteConfig {} -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/extension/podcastserver/item/Slugable.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.extension.podcastserver.item 2 | 3 | import java.nio.file.Path 4 | import java.text.Normalizer 5 | import java.util.* 6 | import kotlin.io.path.extension 7 | 8 | interface Sluggable { 9 | 10 | val title: String 11 | val mimeType: String 12 | val fileName: Path? 13 | 14 | fun slug(): String { 15 | val extension = fileName?.extension ?: mimeType.substringAfter("/") 16 | val sluggedTitle = Normalizer.normalize(title, Normalizer.Form.NFD) 17 | .lowercase(Locale.getDefault()) 18 | .replace("\\p{IsM}+".toRegex(), "") 19 | .replace("\\p{IsP}+".toRegex(), " ") 20 | .trim() 21 | .replace("\\s+".toRegex(), "-") 22 | .replace("[^a-zA-Z0-9.-]".toRegex(), "_") 23 | 24 | return "$sluggedTitle.$extension" 25 | } 26 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/upload/UploadUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.upload 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.ItemFromUpdate 4 | import com.github.davinkevin.podcastserver.update.updaters.PodcastToUpdate 5 | import com.github.davinkevin.podcastserver.update.updaters.Type 6 | import com.github.davinkevin.podcastserver.update.updaters.Updater 7 | import io.micrometer.core.instrument.MeterRegistry 8 | import java.net.URI 9 | 10 | class UploadUpdater( 11 | override val registry: MeterRegistry, 12 | ) : Updater { 13 | override fun findItems(podcast: PodcastToUpdate): List = emptyList() 14 | override fun signatureOf(url: URI): String = "" 15 | override fun type(): Type = TYPE 16 | override fun compatibility(url: String): Int = Integer.MAX_VALUE 17 | } 18 | 19 | private val TYPE = Type("upload", "Upload") 20 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/rss/RSSUpdaterConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.rss 2 | 3 | import com.github.davinkevin.podcastserver.service.image.ImageService 4 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 5 | import io.micrometer.core.instrument.MeterRegistry 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(ImageServiceConfig::class) 13 | class RSSUpdaterConfig { 14 | 15 | @Bean 16 | fun rssUpdater( 17 | imageService: ImageService, 18 | rcb: RestClient.Builder, 19 | registry: MeterRegistry, 20 | ): RSSUpdater { 21 | return RSSUpdater(imageService, rcb.clone(), registry) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/update/updaters/TypeTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Created by kevin on 28/06/15 for Podcast Server 8 | */ 9 | class TypeTest { 10 | 11 | @Test 12 | fun should_have_key_and_name() { 13 | /* Given */ 14 | /* When */ 15 | val type = Type("Key", "Value") 16 | /* Then */ 17 | assertThat(type.key).isEqualTo("Key") 18 | assertThat(type.name).isEqualTo("Value") 19 | } 20 | 21 | @Test 22 | fun `should be equal if has same key and name`() { 23 | /* Given */ 24 | val k = "key" 25 | val n = "name" 26 | /* When */ 27 | val t1 = Type(k, n) 28 | val t2 = Type(k, n) 29 | /* Then */ 30 | assertThat(t1).isEqualTo(t2) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/utils/MatcherExtractorTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | import java.util.regex.Pattern 6 | 7 | class MatcherExtractorTest { 8 | 9 | @Test 10 | fun `should extract value`() { 11 | /* GIVEN */ 12 | val s = "foo" 13 | val p = Pattern.compile("(.*)") 14 | 15 | /* WHEN */ 16 | val v = MatcherExtractor.from(p).on(s).group(1) 17 | 18 | /* THEN */ 19 | assertThat(v).isEqualTo("foo") 20 | } 21 | 22 | @Test 23 | fun `should not return any value`() { 24 | /* GIVEN */ 25 | val s = "" 26 | val p = Pattern.compile("abc") 27 | 28 | /* WHEN */ 29 | val v = MatcherExtractor.from(p).on(s).group(1) 30 | 31 | /* THEN */ 32 | assertThat(v).isNull() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | import { AppState } from '../app.reducer'; 6 | import { Podcast } from '../shared/entity'; 7 | import { podcasts } from '#app/podcasts/podcasts.reducer'; 8 | 9 | @Component({ 10 | selector: 'ps-podcasts', 11 | templateUrl: './podcasts.component.html', 12 | styleUrls: ['./podcasts.component.scss'] 13 | }) 14 | export class PodcastsComponent implements OnInit { 15 | podcasts: Podcast[]; 16 | 17 | constructor(private store: Store) {} 18 | 19 | ngOnInit() { 20 | this.store.pipe(select(podcasts), map(toPodcastOrderedByDate)).subscribe(d => (this.podcasts = d)); 21 | } 22 | } 23 | 24 | function toPodcastOrderedByDate(p: Podcast[]) { 25 | return p.sort((a: Podcast, b: Podcast) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()); 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/download/downloaders/DownloaderSelector.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.download.downloaders 2 | 3 | import com.github.davinkevin.podcastserver.download.downloaders.noop.NoOpDownloaderFactory 4 | import org.springframework.context.ApplicationContext 5 | import org.springframework.stereotype.Service 6 | 7 | /** 8 | * Created by kevin on 17/03/15. 9 | */ 10 | @Service 11 | class DownloaderSelector( 12 | val context: ApplicationContext, 13 | val downloaderFactories: Set 14 | ) { 15 | 16 | fun of(information: DownloadingInformation): DownloaderFactory { 17 | if (information.urls.isEmpty()) { 18 | return NoOpDownloaderFactory 19 | } 20 | 21 | return downloaderFactories.minByOrNull { it.compatibility(information) }!! 22 | } 23 | 24 | companion object { 25 | val NoOpDownloaderFactory = NoOpDownloaderFactory() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/core/podcast.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | import { select, Store } from '@ngrx/store'; 4 | import { FindOneAction } from '../podcast.actions'; 5 | import { selectPodcast } from '../podcast.reducer'; 6 | import { Injectable } from '@angular/core'; 7 | import { skip, take } from 'rxjs/operators'; 8 | import { Podcast } from '#app/shared/entity'; 9 | import { AppState } from '#app/app.reducer'; 10 | 11 | @Injectable() 12 | export class PodcastResolver implements Resolve { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | this.store.dispatch(new FindOneAction(route.params.id)); 17 | 18 | return this.store.pipe(select(selectPodcast), skip(1), take(1)); 19 | } 20 | } 21 | 22 | export const toPodcast = (d: any) => d.podcast; 23 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/main/java/com/gitlab/davinkevin/podcastserver/youtubedl/YoutubeDLException.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl; 2 | 3 | /** 4 | * YoutubeDL Exception 5 | */ 6 | public class YoutubeDLException extends Exception { 7 | 8 | /** 9 | * Exception message 10 | */ 11 | private String message; 12 | 13 | /** 14 | * Construct YoutubeDLException with a message 15 | * @param message 16 | */ 17 | public YoutubeDLException(String message) { 18 | this.message = message; 19 | } 20 | 21 | /** 22 | * Construct YoutubeDLException from another exception 23 | * @param e Any exception 24 | */ 25 | public YoutubeDLException(Exception e) { 26 | message = e.getMessage(); 27 | } 28 | 29 | /** 30 | * Get exception message 31 | * @return exception message 32 | */ 33 | @Override 34 | public String getMessage() { 35 | return message; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/noop/NoOpFinder.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.noop 2 | 3 | import com.github.davinkevin.podcastserver.find.FindPodcastInformation 4 | import com.github.davinkevin.podcastserver.find.finders.Finder 5 | import org.slf4j.LoggerFactory 6 | import java.net.URI 7 | 8 | /** 9 | * Created by kevin on 08/03/2016 for Podcast Server 10 | */ 11 | class NoOpFinder : Finder { 12 | 13 | private val log = LoggerFactory.getLogger(NoOpFinder::class.java) 14 | 15 | override fun findPodcastInformation(url: String): FindPodcastInformation { 16 | log.warn("Using Noop finder for url {}", url) 17 | 18 | return FindPodcastInformation( 19 | title = "", 20 | url = URI(url), 21 | description = "", 22 | type = "noop", 23 | cover = null 24 | ) 25 | } 26 | 27 | override fun compatibility(url: String): Int = Int.MAX_VALUE 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ############################## 2 | # Spring Config # 3 | ############################## 4 | spring: 5 | main: 6 | banner-mode: off 7 | codec: 8 | max-in-memory-size: 500MB 9 | threads: 10 | virtual: 11 | enabled: true 12 | servlet: 13 | multipart: 14 | max-file-size: 10GB 15 | max-request-size: 10GB 16 | server: 17 | port: 8080 18 | 19 | 20 | management: 21 | endpoint: 22 | health: 23 | probes: 24 | enabled: true 25 | show-details: always 26 | group: 27 | liveness.include: ["db", "ping"] 28 | readiness.include: ["ping"] 29 | endpoints: 30 | web: 31 | exposure: 32 | include: ['health', 'info', 'env', 'configprops', 'prometheus', 'metrics'] 33 | 34 | ############################## 35 | # Application Specific # 36 | ############################## 37 | 38 | podcastserver: 39 | max-update-parallels: 256 40 | 41 | logging: 42 | level: 43 | org: 44 | apache: 45 | catalina: off 46 | -------------------------------------------------------------------------------- /frontend-angular/src/app/search/resolver/search.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { map, skip, take } from 'rxjs/operators'; 3 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 4 | import { Item, Page } from '#app/shared/entity'; 5 | import { Observable } from 'rxjs'; 6 | import { select, Store } from '@ngrx/store'; 7 | import { searchRequest, searchResults } from '../search.reducer'; 8 | import { AppState } from '#app/app.reducer'; 9 | import { Search } from '#app/search/search.actions'; 10 | 11 | @Injectable() 12 | export class SearchResolver implements Resolve> { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { 16 | this.store.pipe(select(searchRequest), map(r => new Search(r))) 17 | .subscribe(v => this.store.dispatch(v)); 18 | 19 | return this.store.pipe(select(searchResults), skip(1), take(1)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/urlService/relative.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=199280,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=256x144 4 | 9dce76b19072beda39720aa04aa2e47a-video=118000-audio_AACL_fra_70000_315=70000.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=328600,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=320x180 6 | 9dce76b19072beda39720aa04aa2e47a-video=240000-audio_AACL_fra_70000_315=70000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=616920,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=512x288 8 | 9dce76b19072beda39720aa04aa2e47a-video=512000-audio_AACL_fra_70000_315=70000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=888280,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=704x396 10 | 9dce76b19072beda39720aa04aa2e47a-video=768000-audio_AACL_fra_70000_315=70000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1562440,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=1024x576 12 | 9dce76b19072beda39720aa04aa2e47a-video=1404000-audio_AACL_fra_70000_315=70000.m3u8 13 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/core/podcast-items.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; 2 | import { select, Store } from '@ngrx/store'; 3 | import { Injectable } from '@angular/core'; 4 | import { Observable } from 'rxjs'; 5 | import { selectPodcastItems } from '../podcast.reducer'; 6 | import { skip, take } from 'rxjs/operators'; 7 | import { Direction, Item, Page } from '#app/shared/entity'; 8 | import { FindItemsByPodcastsAndPageAction } from '../podcast.actions'; 9 | import { AppState } from '#app/app.reducer'; 10 | 11 | @Injectable() 12 | export class PodcastItemsResolver implements Resolve> { 13 | constructor(private store: Store) {} 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { 16 | this.store.dispatch( 17 | new FindItemsByPodcastsAndPageAction(route.params.id) 18 | ); 19 | 20 | return this.store.pipe(select(selectPodcastItems), skip(1), take(1)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 7 |
8 | 9 |
10 | - 11 | 12 | + 13 |
14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/francetv/FranceTvFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.francetv 2 | 3 | import com.github.davinkevin.podcastserver.service.image.ImageService 4 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.context.annotation.Import 8 | import org.springframework.web.client.RestClient 9 | 10 | /** 11 | * Created by kevin on 01/11/2019 12 | */ 13 | @Configuration 14 | @Import(ImageServiceConfig::class) 15 | class FranceTvFinderConfig { 16 | 17 | @Bean 18 | fun franceTvFinder(rcb: RestClient.Builder, imageService: ImageService): FranceTvFinder { 19 | val client = rcb.clone() 20 | .baseUrl("https://www.france.tv/") 21 | .build() 22 | 23 | return FranceTvFinder(client, imageService) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/UpdateHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update 2 | 3 | import org.springframework.web.servlet.function.ServerRequest 4 | import org.springframework.web.servlet.function.ServerResponse 5 | import org.springframework.web.servlet.function.paramOrNull 6 | import java.util.* 7 | 8 | class UpdateHandler( 9 | private val update: UpdateService 10 | ) { 11 | 12 | fun updateAll(r: ServerRequest): ServerResponse { 13 | val force = r.paramOrNull("force")?.toBoolean() ?: false 14 | val withDownload = r.paramOrNull("download")?.toBoolean() ?: false 15 | 16 | update.updateAll(force, withDownload) 17 | 18 | return ServerResponse.ok().build() 19 | } 20 | 21 | fun update(r: ServerRequest): ServerResponse { 22 | val id = r.pathVariable("podcastId") 23 | .let(UUID::fromString) 24 | 25 | update.update(id) 26 | 27 | return ServerResponse.ok().build() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/test/resources/xml/podcast-with-port-not-defined-and-http.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Podcast title 5 | http://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/rss 6 | desc 7 | desc 8 | desc 9 | fr-fr 10 | RSS 11 | 12 | Sun, 31 Mar 2019 11:21:32 +0100 13 | http://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 14 | 15 | 200 16 | http://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 17 | 200 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/rss/RSSFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.rss 2 | 3 | import com.github.davinkevin.podcastserver.extension.restclient.withStringUTF8MessageConverter 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(ImageServiceConfig::class) 13 | class RSSFinderConfig { 14 | 15 | @Bean 16 | fun rssFinder( 17 | imageService: ImageService, 18 | rcb: RestClient.Builder 19 | ): RSSFinder { 20 | val client = rcb.clone() 21 | .withStringUTF8MessageConverter() 22 | 23 | return RSSFinder(imageService, client) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/test/resources/xml/podcast-with-port-not-defined-and-https.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Podcast title 5 | https://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/rss 6 | desc 7 | desc 8 | desc 9 | fr-fr 10 | RSS 11 | 12 | Sun, 31 Mar 2019 11:21:32 +0100 13 | https://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 14 | 15 | 200 16 | https://localhost/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 17 | 200 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.cover 2 | 3 | import com.github.davinkevin.podcastserver.service.storage.CoverExistsRequest 4 | import com.github.davinkevin.podcastserver.service.storage.DeleteRequest 5 | import com.github.davinkevin.podcastserver.service.storage.FileStorageService 6 | import java.time.OffsetDateTime 7 | 8 | class CoverService( 9 | private val cover: CoverRepository, 10 | private val file: FileStorageService 11 | ) { 12 | fun deleteCoversInFileSystemOlderThan(date: OffsetDateTime) { 13 | cover 14 | .findCoverOlderThan(date) 15 | .asSequence() 16 | .filter { file.coverExists(it.toCoverExistsRequest()) != null } 17 | .forEach { file.delete(it) } 18 | } 19 | 20 | } 21 | 22 | private fun DeleteRequest.ForCover.toCoverExistsRequest() = CoverExistsRequest.ForItem( 23 | id = id, 24 | podcastTitle = podcast.title, 25 | coverExtension = extension 26 | ) 27 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/itunes/ItunesFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.itunes 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.github.davinkevin.podcastserver.find.finders.rss.RSSFinder 5 | import com.github.davinkevin.podcastserver.find.finders.rss.RSSFinderConfig 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(RSSFinderConfig::class) 13 | class ItunesFinderConfig { 14 | 15 | @Bean 16 | fun itunesFinder(om: ObjectMapper, rssFinder: RSSFinder, rcb: RestClient.Builder): ItunesFinder { 17 | val rc = rcb 18 | .clone() 19 | .baseUrl("https://itunes.apple.com/") 20 | .build() 21 | 22 | return ItunesFinder(rssFinder, rc, om) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /backend/src/test/resources/xml/podcast-with-x-forwarded-port.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Podcast title 5 | https://localhost:1234/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/rss 6 | desc 7 | desc 8 | desc 9 | fr-fr 10 | RSS 11 | 12 | Sun, 31 Mar 2019 11:21:32 +0100 13 | 14 | 200 15 | https://localhost:1234/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 16 | 200 17 | 18 | https://localhost:1234/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend-angular/src/app/floating-player/floating-player.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Player 5 | 6 | close 7 | 8 |
9 | 10 |
11 | 12 | 15 | 16 |
17 | cover 18 | 21 |
22 | 23 |
24 |

This item isn't readable inside your browser...

25 |
26 | 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/gulli/GulliUpdaterConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.gulli 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import io.micrometer.core.instrument.MeterRegistry 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.web.client.RestClient 9 | 10 | /** 11 | * Created by kevin on 14/03/2020 12 | */ 13 | @Configuration 14 | class GulliUpdaterConfig { 15 | 16 | @Bean 17 | fun gulliUpdater( 18 | rcb: RestClient.Builder, 19 | image: ImageService, 20 | mapper: ObjectMapper, 21 | registry: MeterRegistry, 22 | ): GulliUpdater { 23 | val wc = rcb.clone() 24 | .baseUrl("https://replay.gulli.fr") 25 | .build() 26 | 27 | return GulliUpdater(wc, image, mapper, registry) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/dailymotion/DailymotionFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.dailymotion 2 | 3 | import com.github.davinkevin.podcastserver.service.image.ImageService 4 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.context.annotation.Import 8 | import org.springframework.web.client.RestClient 9 | 10 | /** 11 | * Created by kevin on 01/11/2019 12 | */ 13 | @Configuration 14 | @Import(ImageServiceConfig::class) 15 | class DailymotionFinderConfig { 16 | 17 | @Bean 18 | fun dailymotionFinder(rcb: RestClient.Builder, image: ImageService): DailymotionFinder { 19 | val client = rcb 20 | .clone() 21 | .baseUrl("https://api.dailymotion.com/") 22 | .build() 23 | 24 | return DailymotionFinder(client, image) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/utils/multipart/outputfile.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.actions.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import { Item, uuid } from '#app/shared/entity'; 3 | 4 | export enum AppAction { 5 | OPEN_SIDE_NAV = '[SideNav] Open SideNav', 6 | CLOSE_SIDE_NAV = '[SideNav] Close SideNav', 7 | DOWNLOAD_ITEM = '[Download] Download item', 8 | DOWNLOAD_PROGRESS = '[Download] Download progressing', 9 | } 10 | 11 | export class OpenSideNavAction implements Action { 12 | readonly type = AppAction.OPEN_SIDE_NAV; 13 | } 14 | export class CloseSideNavAction implements Action { 15 | readonly type = AppAction.CLOSE_SIDE_NAV; 16 | } 17 | export class DownloadItemAction implements Action { 18 | readonly type = AppAction.DOWNLOAD_ITEM; 19 | constructor(public itemId: uuid, public podcastId: uuid) {} 20 | } 21 | export class DownloadProgressAction implements Action { 22 | readonly type = AppAction.DOWNLOAD_PROGRESS; 23 | constructor(public item: Item) {} 24 | } 25 | 26 | export type AppActions 27 | = OpenSideNavAction 28 | | CloseSideNavAction 29 | | DownloadProgressAction 30 | ; 31 | -------------------------------------------------------------------------------- /backend/src/test/resources/xml/podcast-with-lots-of-parameters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Podcast title 5 | https://localhost:8080/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/rss?v=1234&limit=false 6 | desc 7 | desc 8 | desc 9 | fr-fr 10 | RSS 11 | 12 | Sun, 31 Mar 2019 11:21:32 +0100 13 | 14 | 200 15 | https://localhost:8080/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 16 | 200 17 | 18 | https://localhost:8080/api/v1/podcasts/dd16b2eb-657e-4064-b470-5b99397ce729/cover.png 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend-angular/src/app/item/item.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { Item, Podcast } from '../shared/entity'; 4 | 5 | import { ItemAction, ItemActions } from './item.actions'; 6 | 7 | export interface ItemState { 8 | item: Item; 9 | podcast: Podcast; 10 | } 11 | 12 | const initialState: ItemState = { 13 | item: null, 14 | podcast: null 15 | }; 16 | 17 | export function itemReducer(state = initialState, action: ItemActions): ItemState { 18 | switch (action.type) { 19 | case ItemAction.FIND_ONE_SUCCESS: { 20 | return { ...state, item: action.item }; 21 | } 22 | 23 | case ItemAction.FIND_PARENT_PODCAST_SUCCESS: { 24 | return { ...state, podcast: action.podcast }; 25 | } 26 | 27 | default: { 28 | return state; 29 | } 30 | } 31 | } 32 | 33 | const moduleSelector = createFeatureSelector('item'); 34 | export const item = createSelector(moduleSelector, (s: ItemState) => s.item); 35 | export const podcast = createSelector(moduleSelector, (s: ItemState) => s.podcast); 36 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/dailymotion/DailymotionUpdaterConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.dailymotion 2 | 3 | import com.github.davinkevin.podcastserver.service.image.ImageService 4 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 5 | import io.micrometer.core.instrument.MeterRegistry 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(ImageServiceConfig::class) 13 | class DailymotionUpdaterConfig { 14 | 15 | @Bean 16 | fun dailymotionUpdater( 17 | rcb: RestClient.Builder, 18 | image: ImageService, 19 | registry: MeterRegistry, 20 | ): DailymotionUpdater { 21 | val rc = rcb.clone().baseUrl("https://api.dailymotion.com").build() 22 | 23 | return DailymotionUpdater(rc, image, registry) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend-angular/src/app/search/resolver/search-query.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { inject, TestBed } from '@angular/core/testing'; 3 | 4 | import { SearchQueryResolver } from './search-query.resolver'; 5 | import { Store, StoreModule } from '@ngrx/store'; 6 | import * as fromSearch from '../search.reducer'; 7 | 8 | describe('SearchQueryResolver', () => { 9 | let store, resolver; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [StoreModule.forRoot([]), StoreModule.forFeature('search', fromSearch.search)], 14 | providers: [SearchQueryResolver] 15 | }); 16 | resolver = TestBed.get(SearchQueryResolver); 17 | }); 18 | 19 | beforeEach(() => { 20 | store = TestBed.get(Store); 21 | spyOn(store, 'dispatch').and.callThrough(); 22 | spyOn(store, 'select').and.callThrough(); 23 | }); 24 | 25 | it( 26 | 'should be created', 27 | inject([SearchQueryResolver], (service: SearchQueryResolver) => { 28 | expect(service).toBeTruthy(); 29 | }) 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/vg-link-vlc/vg-link-vlc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 20/02/2016 for Podcast Server 3 | */ 4 | import {Component, Config, Module} from '../../../../decorators'; 5 | import './vg-link-vlc.css!'; 6 | 7 | @Module({ 8 | name : 'ps.common.component.videogular.vgLinkVlc' 9 | }) 10 | @Config($compileProvider => { "ngInject"; return $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|vlc-x-callback):/); }) 11 | @Component({ 12 | selector : 'vg-link-vlc', 13 | as : 'vglinkvlc', 14 | bindings : { url: '=' }, 15 | template : `
` 16 | }) 17 | export default class VgLinkVLC { 18 | 19 | fqURL = "" 20 | 21 | constructor($location) { 22 | "ngInject"; 23 | this.$location = $location; 24 | } 25 | 26 | $onInit() { 27 | this.fqURL = this.$location.protocol() + "://" + this.$location.host() + this.url 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/youtube/YoutubeFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.youtube 2 | 3 | import com.github.davinkevin.podcastserver.extension.restclient.withStringUTF8MessageConverter 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(ImageServiceConfig::class) 13 | class YoutubeFinderConfig { 14 | 15 | @Bean 16 | fun youtubeFinder(imageService: ImageService, rcb: RestClient.Builder): YoutubeFinder { 17 | val builder = rcb 18 | .clone() 19 | .withStringUTF8MessageConverter() 20 | .defaultHeader("User-Agent", "curl/7.64.1") 21 | 22 | return YoutubeFinder(imageService, builder) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/gulli/GulliFinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.gulli 2 | 3 | import com.github.davinkevin.podcastserver.extension.restclient.withStringUTF8MessageConverter 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | @Configuration 12 | @Import(ImageServiceConfig::class) 13 | class GulliFinderConfig { 14 | 15 | @Bean 16 | fun gulliFinder(rcb: RestClient.Builder, image: ImageService): GulliFinder { 17 | val client = rcb 18 | .clone() 19 | .baseUrl("https://replay.gulli.fr") 20 | .withStringUTF8MessageConverter() 21 | .build() 22 | 23 | return GulliFinder(client, image) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/config/config.js: -------------------------------------------------------------------------------- 1 | import {Module} from '../decorators'; 2 | import LoadingBar from './loading'; 3 | import Bootstrap from './bootstrap/bootstrap'; 4 | import ngFileUpload from 'ng-file-upload'; 5 | import PlayerInlineModule from '../common/component/player-inline/player-inline'; 6 | import NavbarModule from '../common/component/navbar/navbar'; 7 | import AuthorizeNotificationModule from '../common/component/authorize-notification/authorize-notification'; 8 | import UpdatingModule from '../common/component/updating/updating'; 9 | import 'angular-touch'; 10 | import 'angular-animate'; 11 | import './bootstrap/bootstrap'; 12 | import './font-awesome/font-awesome'; 13 | import './ionicons/ionicons'; 14 | import './styles/styles'; 15 | 16 | 17 | @Module({ 18 | name : 'ps.config', 19 | modules : [ 20 | 'ngTouch', 21 | 'ngAnimate', 22 | Bootstrap, 23 | ngFileUpload, 24 | NavbarModule, 25 | AuthorizeNotificationModule, 26 | LoadingBar, 27 | PlayerInlineModule, 28 | UpdatingModule 29 | ] 30 | }) 31 | export default class Config{} -------------------------------------------------------------------------------- /frontend-angular/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.gradle.node.yarn.task.YarnTask 2 | 3 | plugins { 4 | base 5 | id("com.github.node-gradle.node") version "3.5.1" 6 | } 7 | 8 | group = "com.github.davinkevin.podcastserver" 9 | version = "2025.7.0" 10 | description = "frontend-angular" 11 | 12 | node { 13 | download.set(true) 14 | version.set("9.11.2") 15 | yarnVersion.set("1.7.0") 16 | } 17 | 18 | project.tasks["yarn_test"].dependsOn("yarn") 19 | 20 | tasks.register("downloadDependencies") { 21 | dependsOn("nodeSetup", "yarnSetup", "yarn") 22 | } 23 | 24 | tasks.named("yarn") { 25 | args.addAll("--network-timeout", "100000") 26 | } 27 | 28 | tasks.named("yarn_build") { 29 | inputs.dir(file("src")) 30 | .withPropertyName("source") 31 | .withPathSensitivity(PathSensitivity.RELATIVE) 32 | 33 | outputs.dir(file("$projectDir/dist")) 34 | .withPropertyName("dist") 35 | 36 | dependsOn("yarn") 37 | } 38 | 39 | tasks.named("build") { 40 | dependsOn("yarn_build") 41 | } 42 | 43 | tasks.named("clean") { 44 | delete.add("node_modules") 45 | delete.add("dist") 46 | } 47 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/podcast.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { Item, Page, Podcast } from '../shared/entity'; 4 | 5 | import { PodcastActions, PodcastAction } from './podcast.actions'; 6 | 7 | export interface PodcastState { 8 | podcast: Podcast; 9 | items: Page; 10 | } 11 | 12 | const initialState: PodcastState = { 13 | podcast: null, 14 | items: null 15 | }; 16 | 17 | export function reducer(state = initialState, action: PodcastActions): PodcastState { 18 | switch (action.type) { 19 | case PodcastAction.FIND_ONE_SUCCESS: { 20 | return { ...state, podcast: action.podcast }; 21 | } 22 | 23 | case PodcastAction.FIND_ITEMS_SUCCESS: { 24 | return { ...state, items: action.items }; 25 | } 26 | 27 | default: { 28 | return state; 29 | } 30 | } 31 | } 32 | 33 | const moduleSelector = createFeatureSelector('podcast'); 34 | export const selectPodcast = createSelector(moduleSelector, (s: PodcastState) => s.podcast); 35 | export const selectPodcastItems = createSelector(moduleSelector, (s: PodcastState) => s.items); 36 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/utils/custom/ffmpeg/CustomRunProcessFunc.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils.custom.ffmpeg 2 | 3 | import com.github.davinkevin.podcastserver.utils.custom.ffmpeg.ProcessListener.Companion.DEFAULT_PROCESS_LISTENER 4 | import net.bramp.ffmpeg.RunProcessFunction 5 | 6 | /** 7 | * Created by kevin on 24/07/2016. 8 | */ 9 | class CustomRunProcessFunc(private var listeners: List = listOf()) : RunProcessFunction() { 10 | 11 | override fun run(args: List): Process { 12 | val p = super.run(args) 13 | 14 | val toBeRemoved = listeners 15 | .firstOrNull { pl -> args.contains(pl.url) } 16 | ?.withProcess(p) 17 | ?: DEFAULT_PROCESS_LISTENER 18 | 19 | this.listeners = listeners - toBeRemoved 20 | 21 | return p 22 | } 23 | 24 | fun add(pl: ProcessListener): CustomRunProcessFunc { 25 | this.listeners = listeners + pl 26 | return this 27 | } 28 | 29 | operator fun plus(pl: ProcessListener): CustomRunProcessFunc = this.add(pl) 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/utils/custom/ffmpeg/ProcessListenerTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.utils.custom.ffmpeg 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.awaitility.Awaitility.await 5 | import org.junit.jupiter.api.Test 6 | import org.mockito.Mockito 7 | import java.util.concurrent.CompletableFuture 8 | import java.util.concurrent.TimeUnit.MILLISECONDS 9 | import java.util.concurrent.TimeUnit.SECONDS 10 | 11 | class ProcessListenerTest { 12 | 13 | @Test 14 | fun `should wait if no process until one is present`() { 15 | /* Given */ 16 | val pl = ProcessListener("foo") 17 | val aProcess = Mockito.mock(Process::class.java) 18 | var process: Process? = null 19 | 20 | /* When */ 21 | CompletableFuture.runAsync { process = pl.process.get() } 22 | MILLISECONDS.sleep(200) 23 | pl.withProcess(aProcess) 24 | 25 | /* Then */ 26 | await().atMost(5, SECONDS).untilAsserted { 27 | assertThat(process).isNotNull().isSameAs(aProcess) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/properties/ExternalToolsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.properties 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Created by kevin on 13/04/2016 for Podcast Server 8 | */ 9 | class ExternalToolsTest { 10 | 11 | @Test 12 | fun should_have_default_value() { 13 | /* Given */ 14 | /* When */ 15 | val externalTools = ExternalTools() 16 | /* Then */ 17 | assertThat(externalTools.ffmpeg).isEqualTo("/usr/local/bin/ffmpeg") 18 | assertThat(externalTools.rtmpdump).isEqualTo("/usr/local/bin/rtmpdump") 19 | } 20 | 21 | @Test 22 | fun should_change_value() { 23 | /* Given */ 24 | /* When */ 25 | val externalTools = ExternalTools( 26 | ffmpeg = "/tmp/ffmpeg", 27 | rtmpdump = "/tmp/rtmpdump" 28 | ) 29 | /* Then */ 30 | assertThat(externalTools.ffmpeg).isEqualTo("/tmp/ffmpeg") 31 | assertThat(externalTools.rtmpdump).isEqualTo("/tmp/rtmpdump") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /distribution/kubernetes/overlays/podcast.k8s.local/gateways/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | components: 5 | - ../../../base/components/gateway 6 | - ../../../base/components/storage/embedded/gateway 7 | - ../../../base/components/storage/embedded/gateway/console 8 | 9 | patches: 10 | - patch: |- 11 | apiVersion: gateway.networking.k8s.io/v1beta1 12 | kind: HTTPRoute 13 | metadata: 14 | name: not-important 15 | spec: 16 | parentRefs: 17 | - name: gateway 18 | hostnames: 19 | - podcast.k8s.local 20 | target: 21 | group: gateway.networking.k8s.io 22 | version: v1beta1 23 | kind: HTTPRoute 24 | - patch: |- 25 | apiVersion: gateway.networking.k8s.io/v1beta1 26 | kind: HTTPRoute 27 | metadata: 28 | name: console 29 | spec: 30 | parentRefs: 31 | - name: gateway 32 | hostnames: 33 | - minio.podcast.k8s.local 34 | target: 35 | group: gateway.networking.k8s.io 36 | version: v1beta1 37 | kind: HTTPRoute 38 | name: console 39 | -------------------------------------------------------------------------------- /frontend-angular/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "globals": { 3 | "ts-jest": { 4 | "tsConfigFile": "src/tsconfig.spec.json" 5 | }, 6 | "__TRANSFORM_HTML__": true 7 | }, 8 | "transform": { 9 | "^.+\\.(ts|js|html)$": "/node_modules/jest-preset-angular/preprocessor.js", 10 | "^.+\\.js$": "babel-jest" 11 | }, 12 | "testMatch": [ 13 | "**/__tests__/**/*.+(ts|js)?(x)", 14 | "**/+(*.)+(spec|test).+(ts|js)?(x)" 15 | ], 16 | "moduleFileExtensions": [ 17 | "ts", 18 | "js", 19 | "html", 20 | "json" 21 | ], 22 | "moduleNameMapper": { 23 | "#app/(.*)": "/src/app/$1" 24 | }, 25 | "transformIgnorePatterns": [ 26 | "node_modules/(?!@ngrx|ng2-truncate)" 27 | ], 28 | "snapshotSerializers": [ 29 | "/node_modules/jest-preset-angular/AngularSnapshotSerializer.js", 30 | "/node_modules/jest-preset-angular/HTMLCommentSerializer.js" 31 | ], 32 | "preset": "jest-preset-angular", 33 | "setupTestFrameworkScriptFile": "/src/test.ts", 34 | "coveragePathIgnorePatterns": [ 35 | "/node_modules/", 36 | "/src/test.ts" 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/find/finders/mytf1/MyTf1FinderConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.find.finders.mytf1 2 | 3 | import com.github.davinkevin.podcastserver.extension.restclient.withStringUTF8MessageConverter 4 | import com.github.davinkevin.podcastserver.service.image.ImageService 5 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | 11 | /** 12 | * Created by kevin on 12/01/2020 13 | */ 14 | @Configuration 15 | @Import(ImageServiceConfig::class) 16 | class MyTf1FinderConfig { 17 | 18 | @Bean 19 | fun myTf1Finder( 20 | rcb: RestClient.Builder, 21 | image: ImageService 22 | ): MyTf1Finder { 23 | val client = rcb.clone() 24 | .baseUrl("https://www.tf1.fr/") 25 | .withStringUTF8MessageConverter() 26 | .build() 27 | 28 | return MyTf1Finder(client, image) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/authorize-notification/authorize-notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kevin on 25/10/2015 for PodcastServer 3 | */ 4 | import {Component, Module} from '../../../decorators'; 5 | import AngularNotification from '../../../common/modules/angularNotification'; 6 | import template from './authorize-notification.html!text'; 7 | 8 | @Module({ 9 | name : 'ps.common.component.authorize-notification', 10 | modules : [ AngularNotification ] 11 | }) 12 | @Component({ 13 | selector : 'authorize-notification', 14 | as : 'an', 15 | template : template 16 | }) 17 | export default class AuthorizeNotificationComponent { 18 | 19 | constructor($window, $notification) { 20 | "ngInject"; 21 | this.$window = $window; 22 | this.$notification = $notification; 23 | } 24 | 25 | $onInit() { this.state = this.hasToBeShown(); } 26 | 27 | manuallyactivate() { this.$notification.requestPermission().then(() => this.state = this.hasToBeShown()); } 28 | 29 | hasToBeShown() { return (('Notification' in this.$window) && this.$window.Notification.permission === 'default'); } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/storage/database.storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: database 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: database 10 | volumeMounts: 11 | - name: database 12 | mountPath: /var/lib/postgresql/data 13 | volumes: 14 | - name: database 15 | hostPath: 16 | path: /tmp/podcast-server/database/ # {"$ref":"#/definitions/io.k8s.cli.substitutions.install-location.database"} 17 | type: Directory 18 | --- 19 | apiVersion: batch/v1beta1 20 | kind: CronJob 21 | metadata: 22 | name: database-backup 23 | spec: 24 | jobTemplate: 25 | spec: 26 | template: 27 | spec: 28 | containers: 29 | - name: database-backup 30 | volumeMounts: 31 | - name: backup 32 | mountPath: /var/lib/postgresql/data 33 | volumes: 34 | - name: backup 35 | hostPath: 36 | path: /tmp/podcast-server/database/backup/ # {"$ref":"#/definitions/io.k8s.cli.substitutions.install-location.database-backup"} 37 | type: Directory 38 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | search 7 |

Search

8 |
9 | 10 | 11 |

12 | Podcasts 13 |

14 |
15 | 16 | timeline 17 |

Stats

18 |
19 | 20 | ondemand_video 21 |

Player

22 |
23 | 24 | system_update_alt 25 |

v1

26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/test/java/com/gitlab/davinkevin/podcastserver/youtubedl/YoutubeDLRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.gitlab.davinkevin.podcastserver.youtubedl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | public class YoutubeDLRequestTest { 8 | 9 | @Test 10 | public void testBuildOptionStandalone() { 11 | 12 | YoutubeDLRequest request = new YoutubeDLRequest(); 13 | request.setOption("help"); 14 | 15 | assertEquals("--help", request.buildOptions()); 16 | } 17 | 18 | @Test 19 | public void testBuildOptionWithValue() { 20 | 21 | YoutubeDLRequest request = new YoutubeDLRequest(); 22 | request.setOption("password", "1234"); 23 | 24 | assertEquals("--password 1234", request.buildOptions()); 25 | } 26 | 27 | @Test 28 | public void testBuildChainOptionWithValue() { 29 | 30 | YoutubeDLRequest request = new YoutubeDLRequest(); 31 | request.setOption("password", "1234"); 32 | request.setOption("username", "1234"); 33 | 34 | assertEquals("--password 1234 --username 1234", request.buildOptions()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/updaters/francetv/FranceTvUpdaterConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.update.updaters.francetv 2 | 3 | import com.github.davinkevin.podcastserver.service.image.ImageService 4 | import com.github.davinkevin.podcastserver.service.image.ImageServiceConfig 5 | import io.micrometer.core.instrument.MeterRegistry 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | import org.springframework.context.annotation.Import 9 | import org.springframework.web.client.RestClient 10 | import java.time.Clock 11 | 12 | /** 13 | * Created by kevin on 18/02/2020 14 | */ 15 | @Configuration 16 | @Import(ImageServiceConfig::class) 17 | class FranceTvUpdaterConfig { 18 | 19 | @Bean 20 | fun franceTvUpdater( 21 | rcb: RestClient.Builder, 22 | image: ImageService, 23 | clock: Clock, 24 | registry: MeterRegistry, 25 | ): FranceTvUpdater { 26 | val franceTvClient = rcb.clone().baseUrl("https://www.france.tv/").build() 27 | 28 | return FranceTvUpdater(franceTvClient, image, clock, registry) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /.gitlab/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ], 6 | rebaseWhen: 'never', 7 | assignees: [ 8 | '@davinkevin', 9 | ], 10 | ignorePaths: [ 11 | 'frontend-angular/**', 12 | 'frontend-angularjs/**', 13 | ], 14 | packageRules: [ 15 | { 16 | matchPackageNames: [ 17 | 'software.amazon.awssdk:bom', 18 | ], 19 | matchUpdateTypes: [ 20 | 'minor', 21 | 'patch', 22 | 'pin', 23 | 'digest', 24 | ], 25 | automerge: true, 26 | }, 27 | { 28 | versioning: 'regex:^RELEASE\\.(?\\d{4})-(?\\d{2})-(?\\d{2})T\\d{2}-\\d{2}-\\d{2}Z$', 29 | automerge: true, 30 | matchPackageNames: [ 31 | '/^minio/', 32 | ], 33 | }, 34 | { 35 | groupName: 'all-flyway', 36 | groupSlug: 'all-flyway', 37 | matchPackageNames: [ 38 | '/.*flyway.*/', 39 | ], 40 | }, 41 | { 42 | groupName: 'all-kotlin', 43 | groupSlug: 'all-kotlin', 44 | matchPackageNames: [ 45 | '/^org.jetbrains.kotlin.*/', 46 | ], 47 | }, 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidNil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidNil extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_nil.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidNil() { 34 | super("uuid_nil", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidNsDns.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidNsDns extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_ns_dns.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidNsDns() { 34 | super("uuid_ns_dns", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidNsOid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidNsOid extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_ns_oid.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidNsOid() { 34 | super("uuid_ns_oid", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidNsUrl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidNsUrl extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_ns_url.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidNsUrl() { 34 | super("uuid_ns_url", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend-angular/README.md: -------------------------------------------------------------------------------- 1 | # PodcastServer 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 29 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { SearchState } from './search/search.reducer'; 3 | import { PodcastsState } from './podcasts/podcasts.reducer'; 4 | import { PodcastState } from './podcast/podcast.reducer'; 5 | import { ItemState } from './item/item.reducer'; 6 | import { AppAction, AppActions } from './app.actions'; 7 | 8 | export interface AppState { 9 | search: SearchState; 10 | podcasts: PodcastsState; 11 | podcast: PodcastState; 12 | item: ItemState; 13 | } 14 | 15 | export interface State { 16 | open: boolean; 17 | } 18 | 19 | const initialState: State = { 20 | open: false 21 | }; 22 | 23 | export function sidenav(state = initialState, action: AppActions): State { 24 | switch (action.type) { 25 | 26 | case AppAction.OPEN_SIDE_NAV: { 27 | return {...state, open: true }; 28 | } 29 | 30 | case AppAction.CLOSE_SIDE_NAV: { 31 | return {...state, open: false }; 32 | } 33 | 34 | default: {return state; } 35 | 36 | } 37 | } 38 | 39 | const sideNavFeature = createFeatureSelector('sidenav'); 40 | export const selectSideNavOpen = createSelector(sideNavFeature, (s: State) => s.open); 41 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidNsX500.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidNsX500 extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_ns_x500.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidNsX500() { 34 | super("uuid_ns_x500", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/ui-v1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ui-v1 5 | labels: 6 | app: podcast-server 7 | module: frontend 8 | version: v1 9 | spec: 10 | ports: 11 | - name: http 12 | port: 8080 13 | targetPort: http 14 | protocol: TCP 15 | selector: 16 | app: podcast-server 17 | module: frontend 18 | version: v1 19 | serving: "true" 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: ui-v1 25 | spec: 26 | selector: 27 | matchLabels: 28 | app: podcast-server 29 | module: frontend 30 | version: v1 31 | serving: "true" 32 | template: 33 | metadata: 34 | labels: 35 | app: podcast-server 36 | module: frontend 37 | version: v1 38 | serving: "true" 39 | spec: 40 | containers: 41 | - image: podcastserver/ui:latest 42 | imagePullPolicy: IfNotPresent 43 | name: ui 44 | readinessProbe: 45 | httpGet: 46 | path: / 47 | port: http 48 | ports: 49 | - name: http 50 | containerPort: 8080 51 | protocol: TCP 52 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/videogular/videogular.less: -------------------------------------------------------------------------------- 1 | .ps { 2 | .item-player, .video-player { 3 | vg-controls { 4 | .controls-container { 5 | background: -webkit-linear-gradient(top, rgba(69, 72, 77, 0) 19%, rgba(69, 72, 77, 0.0666667) 34%, rgba(69, 72, 77, 0.0784314) 37%, rgba(0, 0, 0, 0.388235) 100%); 6 | } 7 | .show-animation { 8 | animation: psShowControlsAnimationFrames ease-out 0.5s; 9 | animation-iteration-count: 1; 10 | animation-fill-mode: both; 11 | } 12 | .hide-animation { 13 | animation: psHideControlsAnimationFrames ease-out 0.5s; 14 | animation-iteration-count: 1; 15 | animation-fill-mode: both; 16 | } 17 | } 18 | } 19 | 20 | @keyframes psShowControlsAnimationFrames { 21 | 0% { 22 | background-color: transparent; 23 | } 24 | 100% { 25 | background: -webkit-linear-gradient(top, rgba(69, 72, 77, 0) 19%, rgba(69, 72, 77, 0.0666667) 34%, rgba(69, 72, 77, 0.0784314) 37%, rgba(0, 0, 0, 0.388235) 100%); 26 | } 27 | } 28 | 29 | @keyframes psHideControlsAnimationFrames { 30 | 0% { 31 | opacity: 0.3; 32 | } 33 | 100% { 34 | opacity: 0; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidGenerateV1.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidGenerateV1 extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_generate_v1.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidGenerateV1() { 34 | super("uuid_generate_v1", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidGenerateV4.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidGenerateV4 extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_generate_v4.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidGenerateV4() { 34 | super("uuid_generate_v4", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/test/resources/remote/downloader/m3u8/m3u8file.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=199280,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=256x144 4 | http://proxy-91.dailymotion.com/video/221/442/9dce76b19072beda39720aa04aa2e47a-video=118000-audio_AACL_fra_70000_315=70000.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=328600,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=320x180 6 | http://proxy-91.dailymotion.com/video/221/442/9dce76b19072beda39720aa04aa2e47a-video=240000-audio_AACL_fra_70000_315=70000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=616920,CODECS="avc1.66.30,mp4a.40.2",RESOLUTION=512x288 8 | http://proxy-91.dailymotion.com/video/221/442/9dce76b19072beda39720aa04aa2e47a-video=512000-audio_AACL_fra_70000_315=70000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=888280,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=704x396 10 | http://proxy-91.dailymotion.com/video/221/442/9dce76b19072beda39720aa04aa2e47a-video=768000-audio_AACL_fra_70000_315=70000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1562440,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=1024x576 12 | http://proxy-91.dailymotion.com/video/221/442/9dce76b19072beda39720aa04aa2e47a-video=1404000-audio_AACL_fra_70000_315=70000.m3u8 13 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/config/ClockConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.config 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.assertj.core.api.Assertions.within 5 | import org.assertj.core.data.TemporalOffset 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.context.annotation.Import 10 | import org.springframework.test.context.junit.jupiter.SpringExtension 11 | import java.time.Clock 12 | import java.time.ZonedDateTime 13 | import java.time.temporal.ChronoUnit 14 | 15 | /** 16 | * Created by kevin on 09/05/2020 17 | */ 18 | @ExtendWith(SpringExtension::class) 19 | @Import(ClockConfig::class) 20 | class ClockConfigTest( 21 | @Autowired private val clock: Clock 22 | ) { 23 | 24 | @Test 25 | fun `should provide clock sync on system`() { 26 | /* Given */ 27 | val now = ZonedDateTime.now() 28 | /* When */ 29 | val nowOnClock = ZonedDateTime.now(clock) 30 | /* Then */ 31 | assertThat(nowOnClock).isCloseTo(now, within(1, ChronoUnit.SECONDS)) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /backend-lib-database/src/main/java/com/github/davinkevin/podcastserver/database/routines/UuidGenerateV1mc.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package com.github.davinkevin.podcastserver.database.routines; 5 | 6 | 7 | import com.github.davinkevin.podcastserver.database.Public; 8 | 9 | import java.util.UUID; 10 | 11 | import org.jooq.Parameter; 12 | import org.jooq.impl.AbstractRoutine; 13 | import org.jooq.impl.Internal; 14 | import org.jooq.impl.SQLDataType; 15 | 16 | 17 | /** 18 | * This class is generated by jOOQ. 19 | */ 20 | @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) 21 | public class UuidGenerateV1mc extends AbstractRoutine { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | /** 26 | * The parameter public.uuid_generate_v1mc.RETURN_VALUE. 27 | */ 28 | public static final Parameter RETURN_VALUE = Internal.createParameter("RETURN_VALUE", SQLDataType.UUID, false, false); 29 | 30 | /** 31 | * Create a new routine call instance 32 | */ 33 | public UuidGenerateV1mc() { 34 | super("uuid_generate_v1mc", Public.PUBLIC, SQLDataType.UUID); 35 | 36 | setReturnParameter(RETURN_VALUE); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/ffmpeg/FfmpegConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.service.ffmpeg 2 | 3 | import com.github.davinkevin.podcastserver.service.properties.ExternalTools 4 | import com.github.davinkevin.podcastserver.utils.custom.ffmpeg.CustomRunProcessFunc 5 | import net.bramp.ffmpeg.FFmpeg 6 | import net.bramp.ffmpeg.FFmpegExecutor 7 | import net.bramp.ffmpeg.FFprobe 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | 12 | /** 13 | * Created by kevin on 21/05/2016 for Podcast Server 14 | */ 15 | @Configuration 16 | @EnableConfigurationProperties(ExternalTools::class) 17 | class FfmpegConfig { 18 | 19 | @Bean 20 | fun ffmpegService(externalTools: ExternalTools): FfmpegService { 21 | val processFunc = CustomRunProcessFunc() 22 | 23 | val ffmpeg = FFmpeg(externalTools.ffmpeg, processFunc) 24 | val ffprobe = FFprobe(externalTools.ffprobe, processFunc) 25 | 26 | val executor = FFmpegExecutor(ffmpeg, ffprobe) 27 | 28 | return FfmpegService(processFunc, executor, ffprobe) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /frontend-angular/src/app/floating-player/floating-player.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Item } from '#app/shared/entity'; 2 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 3 | import { FloatingPlayerAction, FloatingPlayerActions } from '#app/floating-player/floating-player.actions'; 4 | 5 | export type DisplayState = 'OPENED' | 'CLOSED'; 6 | 7 | export interface FloatingPlayerState { 8 | item: Item; 9 | display: DisplayState; 10 | } 11 | 12 | const initialState: FloatingPlayerState = { 13 | item: null, 14 | display: 'CLOSED' 15 | }; 16 | 17 | export function floatingPlayer(state = initialState, action: FloatingPlayerActions): FloatingPlayerState { 18 | switch (action.type) { 19 | case FloatingPlayerAction.PLAY: { 20 | return { ...state, item: action.item, display: 'OPENED' }; 21 | } 22 | 23 | case FloatingPlayerAction.CLOSE: { 24 | return { ...state, item: null, display: 'CLOSED' }; 25 | } 26 | 27 | default: { 28 | return state; 29 | } 30 | } 31 | } 32 | 33 | const moduleSelector = createFeatureSelector('floatingPlayer'); 34 | export const item = createSelector(moduleSelector, (s: FloatingPlayerState) => s.item); 35 | export const display = createSelector(moduleSelector, (s: FloatingPlayerState) => s.display); 36 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/playlist/Playlist.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.playlist 2 | 3 | import com.github.davinkevin.podcastserver.extension.podcastserver.item.Sluggable 4 | import java.net.URI 5 | import java.nio.file.Path 6 | import java.time.OffsetDateTime 7 | import java.util.* 8 | 9 | /** 10 | * Created by kevin on 2019-07-01 11 | */ 12 | data class Playlist(val id: UUID, val name: String) 13 | 14 | data class PlaylistWithItems( 15 | val id: UUID, 16 | val name: String, 17 | val cover: Cover, 18 | val items: Collection 19 | ) { 20 | 21 | data class Cover(val id: UUID, val url: URI, val height: Int, val width: Int) 22 | 23 | data class Item( 24 | val id: UUID, 25 | override val title: String, 26 | override val fileName: Path?, 27 | 28 | val description: String?, 29 | override val mimeType: String, 30 | val length: Long?, 31 | 32 | val pubDate: OffsetDateTime?, 33 | 34 | val podcast: Podcast, 35 | val cover: Cover 36 | ): Sluggable { 37 | 38 | data class Podcast(val id: UUID, val title: String) 39 | data class Cover(val id: UUID, val width: Int, val height: Int, val url: URI) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/watchlist-chooser/watchlist-chooser.html: -------------------------------------------------------------------------------- 1 | 5 | 18 | --------------------------------------------------------------------------------