├── .envrc ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab └── renovate.json5 ├── .idea └── icon.png ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yaml ├── backend-lib-database ├── build.gradle.kts └── src │ └── main │ ├── docker │ └── Dockerfile │ ├── java │ └── com │ │ └── github │ │ └── davinkevin │ │ └── podcastserver │ │ └── database │ │ ├── DefaultCatalog.java │ │ ├── Indexes.java │ │ ├── Keys.java │ │ ├── Public.java │ │ ├── Tables.java │ │ ├── enums │ │ ├── DownloadingState.java │ │ └── ItemStatus.java │ │ └── tables │ │ ├── Cover.java │ │ ├── DownloadingItem.java │ │ ├── FlywaySchemaHistory.java │ │ ├── Item.java │ │ ├── Podcast.java │ │ ├── PodcastTags.java │ │ ├── Tag.java │ │ ├── WatchList.java │ │ ├── WatchListItems.java │ │ └── records │ │ ├── CoverRecord.java │ │ ├── DownloadingItemRecord.java │ │ ├── FlywaySchemaHistoryRecord.java │ │ ├── ItemRecord.java │ │ ├── PodcastRecord.java │ │ ├── PodcastTagsRecord.java │ │ ├── TagRecord.java │ │ ├── WatchListItemsRecord.java │ │ └── WatchListRecord.java │ └── migrations │ ├── V1__schema.sql │ ├── V2__item-mime-type-not-null.sql │ ├── V3__item-status-number-of-fail-not-null.sql │ ├── V4__cover-url-not-null.sql │ ├── V5__use-database-for-download-manager.sql │ ├── V6__add-guid-support-for-items.sql │ ├── V7__migrate-to-enums-to-items.sql │ └── V8__increase-multiple-varchar-size.sql ├── backend-lib-youtubedl ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── gitlab │ │ └── davinkevin │ │ └── podcastserver │ │ └── youtubedl │ │ ├── DownloadProgressCallback.java │ │ ├── YoutubeDL.java │ │ ├── YoutubeDLException.java │ │ ├── YoutubeDLRequest.java │ │ ├── YoutubeDLResponse.java │ │ ├── mapper │ │ ├── HttpHeader.java │ │ ├── VideoFormat.java │ │ ├── VideoInfo.java │ │ ├── VideoSubtitle.java │ │ └── VideoThumbnail.java │ │ └── utils │ │ ├── StreamGobbler.java │ │ └── StreamProcessExtractor.java │ └── test │ ├── java │ └── com │ │ └── gitlab │ │ └── davinkevin │ │ └── podcastserver │ │ └── youtubedl │ │ ├── YoutubeDLRequestTest.java │ │ ├── YoutubeDLResponseTest.java │ │ └── YoutubeDLTest.java │ └── resources │ └── youtube-dl ├── backend ├── build.gradle.kts └── src │ ├── main │ ├── base-image │ │ └── Dockerfile │ ├── kotlin │ │ └── com │ │ │ └── github │ │ │ └── davinkevin │ │ │ └── podcastserver │ │ │ ├── PodcastServerApplication.kt │ │ │ ├── config │ │ │ ├── BeanConfigScan.kt │ │ │ ├── ClockConfig.kt │ │ │ ├── JacksonConfig.kt │ │ │ └── TomcatConfig.kt │ │ │ ├── cover │ │ │ ├── Cover.kt │ │ │ ├── CoverConfig.kt │ │ │ ├── CoverHandler.kt │ │ │ ├── CoverRepository.kt │ │ │ └── CoverService.kt │ │ │ ├── database │ │ │ ├── PathConverter.kt │ │ │ └── StatusConverter.kt │ │ │ ├── download │ │ │ ├── DownloadConfig.kt │ │ │ ├── DownloadHandler.kt │ │ │ ├── DownloadRepository.kt │ │ │ ├── ItemDownloadManager.kt │ │ │ └── downloaders │ │ │ │ └── youtubedl │ │ │ │ ├── YoutubeDlConfig.kt │ │ │ │ ├── YoutubeDlDownloader.kt │ │ │ │ ├── YoutubeDlService.kt │ │ │ │ └── YoutubeDlUtils.kt │ │ │ ├── entity │ │ │ └── Status.kt │ │ │ ├── extension │ │ │ ├── java │ │ │ │ ├── net │ │ │ │ │ └── URI.kt │ │ │ │ └── util │ │ │ │ │ └── Optional.kt │ │ │ ├── podcastserver │ │ │ │ └── item │ │ │ │ │ └── Slugable.kt │ │ │ ├── restclient │ │ │ │ └── RestClient.kt │ │ │ └── serverRequest │ │ │ │ └── ServerRequestExtension.kt │ │ │ ├── find │ │ │ ├── FindConfig.kt │ │ │ ├── FindHandler.kt │ │ │ ├── FindPocastInformation.kt │ │ │ ├── FindService.kt │ │ │ └── finders │ │ │ │ ├── Finder.kt │ │ │ │ ├── FindersExtension.kt │ │ │ │ ├── dailymotion │ │ │ │ ├── DailymotionFinder.kt │ │ │ │ └── DailymotionFinderConfig.kt │ │ │ │ ├── francetv │ │ │ │ ├── FranceTvFinder.kt │ │ │ │ └── FranceTvFinderConfig.kt │ │ │ │ ├── gulli │ │ │ │ ├── GulliFinder.kt │ │ │ │ └── GulliFinderConfig.kt │ │ │ │ ├── itunes │ │ │ │ ├── ItunesFinder.kt │ │ │ │ └── ItunesFinderConfig.kt │ │ │ │ ├── mytf1 │ │ │ │ ├── MyTf1Finder.kt │ │ │ │ └── MyTf1FinderConfig.kt │ │ │ │ ├── noop │ │ │ │ ├── NoOpFinder.kt │ │ │ │ └── NoopConfig.kt │ │ │ │ ├── rss │ │ │ │ ├── RSSFinder.kt │ │ │ │ └── RSSFinderConfig.kt │ │ │ │ └── youtube │ │ │ │ ├── YoutubeFinder.kt │ │ │ │ └── YoutubeFinderConfig.kt │ │ │ ├── item │ │ │ ├── Item.kt │ │ │ ├── ItemConfig.kt │ │ │ ├── ItemHandler.kt │ │ │ ├── ItemRepository.kt │ │ │ └── ItemService.kt │ │ │ ├── manager │ │ │ ├── downloader │ │ │ │ ├── AbstractDownloader.kt │ │ │ │ ├── Downloader.kt │ │ │ │ ├── DownloadingInformation.kt │ │ │ │ ├── FfmpegDownloader.kt │ │ │ │ ├── NoOpDownloader.kt │ │ │ │ └── RTMPDownloader.kt │ │ │ └── selector │ │ │ │ ├── DownloaderSelector.kt │ │ │ │ └── UpdaterSelector.kt │ │ │ ├── messaging │ │ │ ├── MessageHandler.kt │ │ │ ├── MessagingConfig.kt │ │ │ └── MessagingTemplate.kt │ │ │ ├── playlist │ │ │ ├── Playlist.kt │ │ │ ├── PlaylistConfig.kt │ │ │ ├── PlaylistHandler.kt │ │ │ ├── PlaylistRepository.kt │ │ │ └── PlaylistService.kt │ │ │ ├── podcast │ │ │ ├── Podcast.kt │ │ │ ├── PodcastConfig.kt │ │ │ ├── PodcastHandler.kt │ │ │ ├── PodcastRepository.kt │ │ │ ├── PodcastService.kt │ │ │ ├── PodcastXmlHandler.kt │ │ │ └── type │ │ │ │ ├── TypeConfig.kt │ │ │ │ └── TypeHandler.kt │ │ │ ├── rss │ │ │ ├── Channel.kt │ │ │ ├── Item.kt │ │ │ ├── Namespaces.kt │ │ │ ├── Opml.kt │ │ │ └── Rss.kt │ │ │ ├── service │ │ │ ├── ProcessService.kt │ │ │ ├── ffmpeg │ │ │ │ ├── FfmpegConfig.kt │ │ │ │ └── FfmpegService.kt │ │ │ ├── image │ │ │ │ ├── ImageService.kt │ │ │ │ └── ImageServiceConfig.kt │ │ │ ├── properties │ │ │ │ ├── ExternalTools.kt │ │ │ │ └── PodcastServerParameters.kt │ │ │ └── storage │ │ │ │ ├── FileStorageConfig.kt │ │ │ │ └── FileStorageService.kt │ │ │ ├── tag │ │ │ ├── Tag.kt │ │ │ ├── TagConfig.kt │ │ │ ├── TagHandler.kt │ │ │ ├── TagRepository.kt │ │ │ └── TagService.kt │ │ │ ├── update │ │ │ ├── UpdateConfig.kt │ │ │ ├── UpdateHandler.kt │ │ │ ├── UpdateService.kt │ │ │ └── updaters │ │ │ │ ├── Type.kt │ │ │ │ ├── Updater.kt │ │ │ │ ├── dailymotion │ │ │ │ ├── DailymotionUpdater.kt │ │ │ │ └── DailymotionUpdaterConfig.kt │ │ │ │ ├── francetv │ │ │ │ ├── FranceTvUpdater.kt │ │ │ │ └── FranceTvUpdaterConfig.kt │ │ │ │ ├── gulli │ │ │ │ ├── GulliUpdater.kt │ │ │ │ └── GulliUpdaterConfig.kt │ │ │ │ ├── mytf1 │ │ │ │ ├── MyTf1Updater.kt │ │ │ │ └── MyTf1UpdaterConfig.kt │ │ │ │ ├── rss │ │ │ │ ├── RSSUpdater.kt │ │ │ │ └── RSSUpdaterConfig.kt │ │ │ │ ├── upload │ │ │ │ ├── UploadUpdater.kt │ │ │ │ └── UploadUpdaterConfig.kt │ │ │ │ └── youtube │ │ │ │ ├── YoutubeByApiUpdater.kt │ │ │ │ ├── YoutubeByXmlUpdater.kt │ │ │ │ ├── YoutubeUpdaterConfig.kt │ │ │ │ └── YoutubeUtils.kt │ │ │ └── utils │ │ │ ├── MatcherExtractor.kt │ │ │ └── custom │ │ │ └── ffmpeg │ │ │ ├── CustomRunProcessFunc.kt │ │ │ └── ProcessListener.kt │ └── resources │ │ ├── META-INF │ │ └── additional-spring-configuration-metadata.json │ │ ├── application-local-minio.yml │ │ ├── application-local-pg.yml │ │ ├── application-tools-from-homebrew.yml │ │ └── application.yml │ └── test │ ├── kotlin │ └── com │ │ └── github │ │ └── davinkevin │ │ └── podcastserver │ │ ├── IOUtils.kt │ │ ├── MockServer.kt │ │ ├── PodcastServerApplicationTests.kt │ │ ├── business │ │ └── stats │ │ │ ├── NumberOfItemByDateWrapperTest.kt │ │ │ └── StatsPodcastTypeTest.kt │ │ ├── config │ │ ├── BeanConfigScanTest.kt │ │ ├── ClockConfigTest.kt │ │ └── JacksonConfigTest.kt │ │ ├── cover │ │ ├── CoverConfigTest.kt │ │ ├── CoverHandlerTest.kt │ │ ├── CoverRepositoryTest.kt │ │ └── CoverServiceTest.kt │ │ ├── download │ │ ├── DownloadConfigTest.kt │ │ ├── DownloadHandlerTest.kt │ │ ├── DownloadRepositoryTest.kt │ │ ├── ItemDownloadManagerTest.kt │ │ └── downloaders │ │ │ └── youtubedl │ │ │ ├── YoutubeDlConfigTest.kt │ │ │ ├── YoutubeDlDownloaderTest.kt │ │ │ └── YoutubeDlServiceTest.kt │ │ ├── entity │ │ └── StatusTest.kt │ │ ├── extension │ │ ├── assertthat │ │ │ └── SoftAsserts.kt │ │ ├── json │ │ │ └── JsonAssert.kt │ │ ├── mockmvc │ │ │ └── MockMvcRestExceptionControllerAdvise.kt │ │ └── podcastserver │ │ │ └── item │ │ │ └── SluggableTest.kt │ │ ├── find │ │ ├── FindHandlerTest.kt │ │ ├── FindServiceTest.kt │ │ └── finders │ │ │ ├── dailymotion │ │ │ └── DailymotionFinderTest.kt │ │ │ ├── francetv │ │ │ └── FranceTvFinderTest.kt │ │ │ ├── gulli │ │ │ └── GulliFinderTest.kt │ │ │ ├── itunes │ │ │ └── ItunesFinderTest.kt │ │ │ ├── mytf1 │ │ │ └── MyTf1FinderTest.kt │ │ │ ├── noop │ │ │ └── NoOpFinderTest.kt │ │ │ ├── rss │ │ │ └── RSSFinderTest.kt │ │ │ └── youtube │ │ │ └── YoutubeFinderTest.kt │ │ ├── item │ │ ├── ItemConfigTest.kt │ │ ├── ItemHandlerTest.kt │ │ ├── ItemRepositoryTest.kt │ │ └── ItemServiceTest.kt │ │ ├── manager │ │ ├── downloader │ │ │ ├── DownloaderTest.kt │ │ │ ├── FfmpegDownloaderTest.kt │ │ │ ├── NoOpDownloaderTest.kt │ │ │ └── RTMPDownloaderTest.kt │ │ ├── selector │ │ │ ├── DownloaderSelectorTest.kt │ │ │ └── UpdaterSelectorTest.kt │ │ └── worker │ │ │ └── TypeTest.kt │ │ ├── messaging │ │ ├── MessageHandlerTest.kt │ │ ├── MessageSyncClientTest.kt │ │ └── MessagingTemplateTest.kt │ │ ├── playlist │ │ ├── PlaylistHandlerTest.kt │ │ ├── PlaylistRepositoryTest.kt │ │ └── PlaylistServiceTest.kt │ │ ├── podcast │ │ ├── PodcastHandlerTest.kt │ │ ├── PodcastRepositoryTest.kt │ │ ├── PodcastServiceTest.kt │ │ ├── PodcastXmlHandlerTest.kt │ │ └── type │ │ │ └── TypeHandlerTest.kt │ │ ├── service │ │ ├── ProcessServiceTest.kt │ │ ├── ffmpeg │ │ │ ├── FfmpegConfigTest.kt │ │ │ └── FfmpegServiceTest.kt │ │ ├── image │ │ │ └── ImageServiceTest.kt │ │ ├── properties │ │ │ ├── ExternalToolsTest.kt │ │ │ └── PodcastServerParametersTest.kt │ │ └── storage │ │ │ ├── FileStorageConfigTest.kt │ │ │ └── FileStorageServiceTest.kt │ │ ├── tag │ │ ├── TagHandlerTest.kt │ │ ├── TagRepositoryTest.kt │ │ └── TagServiceTest.kt │ │ ├── update │ │ ├── UpdateHandlerTest.kt │ │ ├── UpdateServiceTest.kt │ │ └── updaters │ │ │ ├── UpdaterTest.kt │ │ │ ├── dailymotion │ │ │ └── DailymotionUpdaterTest.kt │ │ │ ├── francetv │ │ │ └── FranceTvUpdaterTest.kt │ │ │ ├── gulli │ │ │ └── GulliUpdaterTest.kt │ │ │ ├── mytf1 │ │ │ └── MyTf1UpdaterTest.kt │ │ │ ├── rss │ │ │ └── RSSUpdaterTest.kt │ │ │ ├── upload │ │ │ └── UploadUpdaterTest.kt │ │ │ └── youtube │ │ │ ├── YoutubeApiTest.kt │ │ │ ├── YoutubeByApiUpdaterTest.kt │ │ │ └── YoutubeByXmlUpdaterTest.kt │ │ └── utils │ │ ├── MatcherExtractorTest.kt │ │ └── custom │ │ └── ffmpeg │ │ ├── CustomRunProcessFuncTest.kt │ │ └── ProcessListenerTest.kt │ └── resources │ ├── __files │ ├── img │ │ └── image.png │ ├── service │ │ ├── htmlService │ │ │ └── jsoup.html │ │ ├── jdomService │ │ │ ├── invalid.xml │ │ │ └── valid.xml │ │ ├── mimeTypeService │ │ │ └── plain.text.txt │ │ └── urlService │ │ │ ├── canalplus.lepetitjournal.20150707.m3u8 │ │ │ └── relative.m3u8 │ └── utils │ │ └── multipart │ │ ├── file_to_serve.txt │ │ └── outputfile.out │ ├── application.yml │ ├── junit-platform.properties │ ├── logback-test.xml │ ├── mockito-extensions │ └── org.mockito.plugins.MockMaker │ ├── remote │ ├── downloader │ │ ├── m3u8 │ │ │ └── m3u8file.m3u8 │ │ └── rtmpdump │ │ │ └── rtmpdump.txt │ └── podcast │ │ ├── JeuxVideoCom │ │ ├── 3615-usul-la-presse-jv-00118218.htm │ │ ├── after-bit-l-apprentissage-musical-par-le-jeu-video.htm │ │ ├── after-work-until-dawn-toute-la-verite-sur-bibpreu.htm │ │ ├── au-ceur-de-l-histoire-de-the-witcher-3-episode-6.htm │ │ ├── back-to-2009-les-jeux-marquants-de-l-annee-en-images-et-en-musique-de-l-epoque.htm │ │ ├── chroniques-video.htm │ │ ├── coupe-du-monde-jeuxvideo-com-cote-d-ivoire-bresil-finale-playstation-3-ps3-00121775.htm │ │ ├── cover-revisite-un-theme-de-zelda-twilight-princess.htm │ │ ├── crossed-scott-pilgrim-vs-the-world-00120064.htm │ │ ├── diablox9-battlefield-3-derniere-chronique-xbox-360-00000085.htm │ │ ├── draw-in-game-titanfall-xbox-one-chute-de-titan-00119598.htm │ │ ├── enyd-raconte-nous-une-histoire-la-grotte-maudite-00117363.htm │ │ ├── epic-annonce-de-la-nouvelle-chronique-epic-00119905.htm │ │ ├── expeditions-inutiles-millenium-show-00115924.htm │ │ ├── fanta-bob-minecraft-la-map-audiosurf-web-00000046.htm │ │ ├── game-s-up-8-ans-d-assassin-s-creed-en-video.htm │ │ ├── il-etait-une-fois-super-mario-kart-super-nintendo-snes-il-etait-une-fois-mario-kart-00120689.htm │ │ ├── joueur-du-grenier-cool-world-nes-supernes.htm │ │ ├── l-histoire-du-jeu-video-comment-realiser-une-chronique.htm │ │ ├── l-univers-du-jeu-independant-dungeon-kingdom-un-dungeon-crawler-a-la-grimrock.htm │ │ ├── la-faq-chiante-du-grenier.htm │ │ ├── la-reconnaissance-vocale-comment-ca-marche.htm │ │ ├── le-defi-du-challenge-dorian-vs-usul.htm │ │ ├── le-fond-de-l-affaire-les-secrets-de-la-serie-starcraft.htm │ │ ├── les-5-jeux-preferes-de-stargazer.htm │ │ ├── les-gamers-de-l-extreme-tetris-worlds-gameboy-advance-gba-tetris-dans-osiris-00114132.htm │ │ ├── looking-for-games-quel-est-le-mmo-ideal.htm │ │ ├── merci-dorian-le-statut-du-jeu-de-role-dans-le-jeu-video.htm │ │ ├── minecraft-hardcore-pirate-des-cuboides-episode-8.htm │ │ ├── new-gamers-factory-street-fighter-pc-benji-et-mike-decouvrent-street-fighter-00114797.htm │ │ ├── overview-mobile-soldats-inconnus.htm │ │ ├── papy-grenier-final-fantasy-vii.htm │ │ ├── parlons-peu-parlons-pub-special-noel.htm │ │ ├── pause-process-les-effets-de-particules.htm │ │ ├── prison-architect-12eme-et-dernier-episode.htm │ │ ├── scenes-de-jeu-grand-theft-auto-iv-pc-vous-ne-vous-inquietez-pas-pour-votre-ame-00000264.htm │ │ ├── seul-face-aux-tenebres-le-rodeur-de-la-bibliotheque.htm │ │ ├── speed-game-fallout-3-termine-en-25-minutes.htm │ │ ├── spoilers-le-premier-half-life.htm │ │ ├── top-10-des-licences-ressuscitees.htm │ │ ├── tour-de-france-jeuxvideo-com-21eme-etape-evry-paris-champs-elysees-playstation-4-ps4-00121505.htm │ │ ├── versus-just-cause-3-les-versions-pc-de-low-a-ultra-comparees.htm │ │ └── vgm-rayman-origins-brise-la-chaine-du-froid-en-musique.htm │ │ ├── dailymotion │ │ ├── karimdebbache-without-description.json │ │ ├── karimdebbache.1.item-without-cover.json │ │ ├── karimdebbache.1.item.json │ │ ├── karimdebbache.10.items.json │ │ ├── karimdebbache.ids.0.item.json │ │ ├── karimdebbache.ids.1.items.json │ │ ├── karimdebbache.ids.10.items.json │ │ ├── karimdebbache.json │ │ └── user.karimdebbache.json │ │ ├── francetv │ │ └── v6 │ │ │ └── secrets-d-histoire │ │ │ ├── Taskfile.yaml │ │ │ ├── secrets-d-histoire.html │ │ │ ├── toutes-les-videos │ │ │ ├── all-unavailable.html │ │ │ ├── all.html │ │ │ ├── no-item.html │ │ │ └── one-item.html │ │ │ └── videos │ │ │ ├── 2759591-philippe-le-bel-et-l-etrange-affaire-des-templiers.html │ │ │ ├── 2772025-louis-xv-et-la-bete-du-gevaudan.html │ │ │ ├── 3007545-l-incroyable-epopee-de-richard-coeur-de-lion.html │ │ │ ├── 4073119-medecin-il-a-essaye-de-sauver-lady-diana-le-soir-de-l-accident.html │ │ │ ├── 4073167-les-engagements-humanitaires-de-lady-di.html │ │ │ ├── 4095694-elisabeth-ii-notre-reine.html │ │ │ ├── 4116544-la-reine-elizabeth-ii-et-sa-relation-a-la-france.html │ │ │ ├── 4206502-rosa-bonheur-la-fee-des-animaux.html │ │ │ ├── 4211497-paris-2024-stephane-bern-livre-les-secrets-d-une-marche-historique.html │ │ │ ├── 4228084-ragnar-le-viking-qui-a-terrorise-paris.html │ │ │ ├── 4474696-l-homme-au-masque-de-fer.html │ │ │ ├── 4560712-l-amour-fou-d-auguste-rodin-et-camille-claudel.html │ │ │ ├── 4560718-elisabeth-la-drole-de-reine-de-belgique.html │ │ │ ├── 4676977-fuite-a-varennes-la-folle-cavale-de-louis-xvi.html │ │ │ ├── 4768249-d-artagnan-le-mousquetaire-du-roi-soleil.html │ │ │ ├── 4876105-nostradamus-ou-comment-predire-son-avenir.html │ │ │ ├── 5243184-jean-paul-ii-l-athlete-de-dieu.html │ │ │ ├── 5304912-vercingetorix-le-premier-des-gaulois.html │ │ │ ├── 5403759-napoleon-iii-le-dernier-empereur-des-francais.html │ │ │ ├── 5475558-vatel-careme-escoffier-a-la-table-des-rois.html │ │ │ ├── 5587902-arthur-et-les-chevaliers-de-la-table-ronde.html │ │ │ ├── 5672001-au-danemark-le-roi-la-reine-et-le-seduisant-docteur.html │ │ │ ├── 5731266-philippe-v-les-demons-du-roi-d-espagne.html │ │ │ ├── 5766417-marie-madeleine-si-pres-de-jesus.html │ │ │ └── case │ │ │ ├── 5766417.without-application-ld-json.html │ │ │ ├── 5766417.without-pubdate.html │ │ │ └── 5766417.without-videoobject.html │ │ ├── gulli │ │ ├── VOD69210373530000.html │ │ ├── VOD69214276976000.html │ │ ├── VOD69214277046000.html │ │ ├── VOD69218401100000.html │ │ ├── VOD69218401170000.html │ │ ├── VOD69262302148000.html │ │ ├── pokemon.html │ │ ├── pokemon.with-1-item.html │ │ ├── pokemon.with-no-item.html │ │ ├── pokemon.without-cover.html │ │ └── pokemon.without-description.html │ │ ├── itunes │ │ └── lookup.json │ │ ├── mytf1 │ │ ├── 13184238.m3u8 │ │ ├── quotidien-deuxieme-partie-14-juin-2019.html │ │ ├── quotidien.query.root-with-no-cover.json │ │ ├── quotidien.query.root.json │ │ ├── quotidien.query.with-no-items.json │ │ ├── quotidien.root-with-picture-url-relative.html │ │ ├── quotidien.root-without-picture-url.html │ │ ├── quotidien.root-without-picture.html │ │ └── quotidien.root.html │ │ ├── rss │ │ ├── rss.appload.with-only-itunes-cover.xml │ │ ├── rss.appload.with-only-rss-cover.xml │ │ ├── rss.appload.without-any-cover.xml │ │ ├── rss.appload.xml │ │ ├── rss.lesGrandesGueules.withItunesCover.xml │ │ ├── rss.lesGrandesGueules.withoutAnyCover.xml │ │ ├── rss.lesGrandesGueules.withoutChannel.xml │ │ └── rss.lesGrandesGueules.xml │ │ ├── tf1replay │ │ └── quotidien.query.root.json │ │ └── youtube │ │ ├── joueurdugrenier-without-external-id.html │ │ ├── joueurdugrenier.2.json │ │ ├── joueurdugrenier.channel.with-0-item.xml │ │ ├── joueurdugrenier.channel.with-1-item.xml │ │ ├── joueurdugrenier.channel.xml │ │ ├── joueurdugrenier.html │ │ ├── joueurdugrenier.id.json │ │ ├── joueurdugrenier.json │ │ ├── joueurdugrenier.playlist.with-0-item.xml │ │ ├── joueurdugrenier.playlist.with-1-item.xml │ │ ├── joueurdugrenier.playlist.xml │ │ └── joueurdugrenier.withoutDescAndCoverAndTitle.html │ ├── spring.properties │ └── xml │ ├── podcast-with-001-items.xml │ ├── podcast-with-020-items.xml │ ├── podcast-with-123-items.xml │ ├── podcast-with-200-items.xml │ ├── podcast-with-50-items.xml │ ├── podcast-with-lots-of-parameters.xml │ ├── podcast-with-port-not-defined-and-http.xml │ ├── podcast-with-port-not-defined-and-https.xml │ ├── podcast-with-x-forwarded-port.xml │ ├── podcast.output.100.xml │ ├── podcast.output.50.xml │ └── watchlist.output.xml ├── build-logic ├── build-plugin-database │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── DatabasePlugin.kt └── build-plugin-docker-images │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── DockerImagePlugin.kt ├── cliff.toml ├── distribution ├── kpt │ └── podcast-server │ │ ├── Kptfile │ │ ├── fn.yaml │ │ ├── ingress │ │ ├── ingress.yaml │ │ ├── tls.crt │ │ └── tls.key │ │ ├── inventory-template.yaml │ │ ├── kustomization.yaml │ │ └── storage │ │ ├── backend.storage.yaml │ │ ├── database.storage.yaml │ │ └── fs.storage.yaml └── kubernetes │ ├── .infrastructure │ ├── contour │ │ ├── contour-gateway-provisioner.yaml │ │ └── kustomization.yaml │ ├── dns │ │ └── podcast-server.profile │ └── gateway │ │ ├── gateway.yaml │ │ ├── kustomization.yaml │ │ ├── namereference.yaml │ │ ├── tls.crt │ │ └── tls.key │ ├── base │ ├── backend.yaml │ ├── components │ │ ├── backend │ │ │ └── remote-debug │ │ │ │ └── kustomization.yaml │ │ ├── database │ │ │ ├── database.yaml │ │ │ └── kustomization.yaml │ │ ├── gateway │ │ │ ├── backend.yaml │ │ │ ├── frontend.yaml │ │ │ └── kustomization.yaml │ │ ├── ingress │ │ │ ├── ingress.yaml │ │ │ └── kustomization.yaml │ │ ├── namespace │ │ │ ├── kustomization.yaml │ │ │ └── namespace.yaml │ │ └── storage │ │ │ └── embedded │ │ │ ├── backend-set-internal-storage.yaml │ │ │ ├── gateway │ │ │ ├── console │ │ │ │ ├── console.yaml │ │ │ │ └── kustomization.yaml │ │ │ ├── kustomization.yaml │ │ │ └── storage.yaml │ │ │ ├── ingress │ │ │ ├── kustomization.yaml │ │ │ └── minio-console.yaml │ │ │ ├── kustomization.yaml │ │ │ └── storage.yaml │ ├── kustomization.yaml │ ├── ui-v1.yaml │ └── ui-v2.yaml │ └── overlays │ ├── all-in-one │ └── kustomization.yaml │ ├── docker-for-desktop │ ├── ingress │ │ ├── certs │ │ │ ├── add-tls.yaml │ │ │ ├── tls.crt │ │ │ └── tls.key │ │ ├── kustomization.yaml │ │ ├── minio │ │ │ └── kustomization.yaml │ │ └── podcast-server │ │ │ └── kustomization.yaml │ ├── kustomization.yaml │ └── volume.yaml │ └── podcast.k8s.local │ ├── gateways │ └── kustomization.yaml │ ├── kustomization.yaml │ └── volume.yaml ├── documentation ├── antora.yml ├── demo-install.cast ├── demo-magic.sh ├── documentation.yml ├── modules │ └── ROOT │ │ ├── examples │ │ └── installation │ │ │ └── kubernetes │ │ │ ├── demo-install.sh │ │ │ ├── kustomization.yaml │ │ │ └── quick-install.sh │ │ ├── nav.adoc │ │ └── pages │ │ ├── installation │ │ └── kubernetes.adoc │ │ └── introduction.adoc └── supplemental-ui │ └── partials │ └── header-content.hbs ├── fake-external-podcast ├── build.sh └── src │ ├── conf │ └── default.conf │ ├── docker │ └── Dockerfile │ └── podcast │ ├── create-podcast.bash │ ├── fake.jpg │ └── rss.xml ├── frontend-angular ├── .editorconfig ├── .prettierignore ├── README.md ├── angular.json ├── build.gradle.kts ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── jest.config.js ├── package.json ├── prettier.config.js ├── protractor.conf.js ├── proxy-prod.conf.json ├── proxy.conf.json ├── src │ ├── _variables.scss │ ├── app │ │ ├── app.actions.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.effects.ts │ │ ├── app.module.ts │ │ ├── app.reducer.ts │ │ ├── floating-player │ │ │ ├── floating-player.actions.ts │ │ │ ├── floating-player.component.html │ │ │ ├── floating-player.component.scss │ │ │ ├── floating-player.component.spec.ts │ │ │ ├── floating-player.component.ts │ │ │ ├── floating-player.module.ts │ │ │ └── floating-player.reducer.ts │ │ ├── item │ │ │ ├── core │ │ │ │ ├── item.resolver.ts │ │ │ │ └── podcast.resolver.ts │ │ │ ├── item.actions.ts │ │ │ ├── item.component.html │ │ │ ├── item.component.scss │ │ │ ├── item.component.spec.ts │ │ │ ├── item.component.ts │ │ │ ├── item.effects.ts │ │ │ ├── item.module.ts │ │ │ └── item.reducer.ts │ │ ├── podcast │ │ │ ├── core │ │ │ │ ├── episodes │ │ │ │ │ ├── episodes.component.html │ │ │ │ │ ├── episodes.component.scss │ │ │ │ │ ├── episodes.component.spec.ts │ │ │ │ │ └── episodes.component.ts │ │ │ │ ├── podcast-items.resolver.spec.ts │ │ │ │ ├── podcast-items.resolver.ts │ │ │ │ ├── podcast.resolver.spec.ts │ │ │ │ └── podcast.resolver.ts │ │ │ ├── podcast.actions.ts │ │ │ ├── podcast.component.html │ │ │ ├── podcast.component.scss │ │ │ ├── podcast.component.spec.ts │ │ │ ├── podcast.component.ts │ │ │ ├── podcast.effects.ts │ │ │ ├── podcast.module.ts │ │ │ └── podcast.reducer.ts │ │ ├── podcasts │ │ │ ├── core │ │ │ │ └── resolver │ │ │ │ │ ├── podcasts.resolver.spec.ts │ │ │ │ │ └── podcasts.resolver.ts │ │ │ ├── podcasts.actions.ts │ │ │ ├── podcasts.component.html │ │ │ ├── podcasts.component.scss │ │ │ ├── podcasts.component.spec.ts │ │ │ ├── podcasts.component.ts │ │ │ ├── podcasts.effects.ts │ │ │ ├── podcasts.module.ts │ │ │ └── podcasts.reducer.ts │ │ ├── search │ │ │ ├── resolver │ │ │ │ ├── search-query.resolver.spec.ts │ │ │ │ ├── search-query.resolver.ts │ │ │ │ ├── search.resolver.spec.ts │ │ │ │ └── search.resolver.ts │ │ │ ├── search.actions.ts │ │ │ ├── search.component.html │ │ │ ├── search.component.scss │ │ │ ├── search.component.spec.ts │ │ │ ├── search.component.ts │ │ │ ├── search.effects.ts │ │ │ ├── search.module.ts │ │ │ └── search.reducer.ts │ │ └── shared │ │ │ ├── entity.ts │ │ │ ├── service │ │ │ ├── item │ │ │ │ ├── item.service.spec.ts │ │ │ │ └── item.service.ts │ │ │ └── podcast │ │ │ │ ├── podcast.service.spec.ts │ │ │ │ └── podcast.service.ts │ │ │ ├── shared.module.ts │ │ │ └── toolbar │ │ │ ├── toolbar.component.html │ │ │ ├── toolbar.component.scss │ │ │ ├── toolbar.component.spec.ts │ │ │ ├── toolbar.component.ts │ │ │ └── toolbar.module.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── typings.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── frontend-angularjs ├── .envrc ├── .eslintrc ├── .jshintrc ├── build.gradle.kts ├── docker │ ├── Dockerfile │ └── default.conf ├── gulp │ ├── paths.js │ ├── tasks │ │ ├── build.js │ │ ├── fonts.js │ │ ├── less.js │ │ ├── lint.js │ │ ├── maven.js │ │ ├── release.js │ │ ├── serve.js │ │ ├── skaffold.js │ │ └── watch.js │ └── utils.js ├── gulpfile.babel.js ├── npm-shrinkwrap.json ├── package.json └── www │ ├── app │ ├── app.js │ ├── common │ │ ├── component │ │ │ ├── authorize-notification │ │ │ │ ├── authorize-notification.html │ │ │ │ └── authorize-notification.js │ │ │ ├── copy │ │ │ │ └── copy.js │ │ │ ├── item-menu │ │ │ │ ├── item-menu.html │ │ │ │ ├── item-menu.js │ │ │ │ └── item-menu.less │ │ │ ├── navbar │ │ │ │ ├── navbar.html │ │ │ │ ├── navbar.js │ │ │ │ └── navbar.less │ │ │ ├── player-inline │ │ │ │ ├── player-inline.html │ │ │ │ ├── player-inline.js │ │ │ │ └── player-inline.less │ │ │ ├── title │ │ │ │ └── title.js │ │ │ ├── updating │ │ │ │ ├── updating.html │ │ │ │ ├── updating.js │ │ │ │ └── updating.less │ │ │ ├── videogular │ │ │ │ ├── vg-copy │ │ │ │ │ ├── vg-copy.js │ │ │ │ │ └── vg-copy.less │ │ │ │ ├── vg-link │ │ │ │ │ ├── vg-link.js │ │ │ │ │ └── vg-link.less │ │ │ │ ├── videogular.js │ │ │ │ └── videogular.less │ │ │ └── watchlist-chooser │ │ │ │ ├── watchlist-chooser.html │ │ │ │ ├── watchlist-chooser.js │ │ │ │ └── watchlist-chooser.less │ │ ├── filter │ │ │ └── html2plainText.js │ │ ├── modules │ │ │ ├── angularNotification.js │ │ │ ├── highCharts.js │ │ │ ├── ngTagsInput.js │ │ │ └── truncate.js │ │ └── service │ │ │ ├── data │ │ │ ├── downloadManager.js │ │ │ ├── itemService.js │ │ │ ├── podcastService.js │ │ │ ├── statsService.js │ │ │ ├── tagService.js │ │ │ ├── typeService.js │ │ │ ├── updateService.js │ │ │ └── watchlistService.js │ │ │ ├── device-detection.js │ │ │ ├── playlistService.js │ │ │ └── title.service.js │ ├── config │ │ ├── bootstrap │ │ │ ├── bootstrap.js │ │ │ └── bootstrap.less │ │ ├── config.js │ │ ├── font-awesome │ │ │ ├── font-awesome.js │ │ │ └── font-awesome.less │ │ ├── ionicons │ │ │ ├── ionicons.js │ │ │ └── ionicons.less │ │ ├── loading.js │ │ ├── route.js │ │ └── styles │ │ │ ├── bootstrap-adaptation.less │ │ │ ├── podcastserver.less │ │ │ ├── styles.js │ │ │ └── tags-input-bootstrap.less │ ├── decorators.js │ ├── download │ │ ├── download.html │ │ └── download.js │ ├── item │ │ ├── details │ │ │ ├── item-details.html │ │ │ └── item.details.js │ │ ├── item.js │ │ └── player │ │ │ ├── item-player.html │ │ │ └── item.player.js │ ├── player │ │ ├── player.html │ │ ├── player.js │ │ └── player.less │ ├── podcasts │ │ ├── creation │ │ │ ├── creation.html │ │ │ └── creation.js │ │ ├── details │ │ │ ├── details.html │ │ │ ├── details.js │ │ │ ├── edition │ │ │ │ ├── edition.html │ │ │ │ └── edition.js │ │ │ ├── episodes │ │ │ │ ├── episodes.html │ │ │ │ └── episodes.js │ │ │ ├── stats │ │ │ │ ├── stats.html │ │ │ │ └── stats.js │ │ │ └── upload │ │ │ │ ├── upload.html │ │ │ │ └── upload.js │ │ ├── podcasts.html │ │ ├── podcasts.js │ │ └── podcasts.less │ ├── search │ │ ├── search.html │ │ ├── search.js │ │ └── search.less │ └── stats │ │ ├── stats.html │ │ └── stats.js │ ├── config.js │ └── index.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── skaffold.yaml ├── storage └── Dockerfile └── ui ├── compile.sh └── src └── docker ├── Dockerfile └── config.toml /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | matchPackageNames: ["software.amazon.awssdk:bom"], 16 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 17 | automerge: true, 18 | }, { 19 | "packagePatterns": ["^minio"], 20 | "versioning": "regex:^RELEASE\\.(?\\d{4})-(?\\d{2})-(?\\d{2})T\\d{2}-\\d{2}-\\d{2}Z$", 21 | automerge: true 22 | }, { 23 | matchPackagePatterns: [".*flyway.*"], 24 | groupName: "all-flyway", 25 | groupSlug: "all-flyway" 26 | }, { 27 | matchPackagePatterns: ["^org.jetbrains.kotlin.*"], 28 | groupName: "all-kotlin", 29 | groupSlug: "all-kotlin" 30 | }] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/.idea/icon.png -------------------------------------------------------------------------------- /backend-lib-database/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.9.0 2 | 3 | FROM flyway/flyway:10.17.0 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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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); -------------------------------------------------------------------------------- /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, long etaInSeconds); 5 | } 6 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-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-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-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 | -------------------------------------------------------------------------------- /backend-lib-youtubedl/src/test/resources/youtube-dl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/backend-lib-youtubedl/src/test/resources/youtube-dl -------------------------------------------------------------------------------- /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/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/config/JacksonConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.config 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.SerializationFeature 5 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 7 | import com.fasterxml.jackson.module.kotlin.kotlinModule 8 | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | import java.nio.file.Path 12 | 13 | /** 14 | * Created by kevin on 15/06/2016 for Podcast Server 15 | */ 16 | @Configuration 17 | class JacksonConfig { 18 | 19 | @Bean 20 | fun mapperCustomization() = Jackson2ObjectMapperBuilderCustomizer { 21 | it.featuresToDisable( 22 | SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, 23 | DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 24 | ) 25 | .serializerByType(Path::class.java, ToStringSerializer()) 26 | .modules( 27 | JavaTimeModule(), 28 | kotlinModule() 29 | ) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/cover/CoverService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.cover 2 | 3 | import com.github.davinkevin.podcastserver.service.storage.FileStorageService 4 | import java.time.OffsetDateTime 5 | 6 | class CoverService( 7 | private val cover: CoverRepository, 8 | private val file: FileStorageService 9 | ) { 10 | fun deleteCoversInFileSystemOlderThan(date: OffsetDateTime) { 11 | cover 12 | .findCoverOlderThan(date) 13 | .asSequence() 14 | .filter { file.coverExists(it.podcast.title, it.item.id, it.extension) != null } 15 | .forEach { file.deleteCover(it) } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/manager/downloader/Downloader.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.manager.downloader 2 | 3 | import com.github.davinkevin.podcastserver.download.ItemDownloadManager 4 | 5 | 6 | interface Downloader : Runnable { 7 | 8 | val downloadingInformation: DownloadingInformation 9 | 10 | fun with(information: DownloadingInformation, itemDownloadManager: ItemDownloadManager): Downloader 11 | 12 | fun download(): DownloadingItem 13 | 14 | fun startDownload() 15 | fun stopDownload() 16 | fun failDownload() 17 | fun finishDownload() 18 | 19 | fun compatibility(downloadingInformation: DownloadingInformation): Int 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/NoOpDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.manager.downloader 2 | 3 | import com.github.davinkevin.podcastserver.download.ItemDownloadManager 4 | 5 | 6 | /** 7 | * Created by kevin on 10/03/2016 for Podcast Server 8 | */ 9 | class NoOpDownloader : Downloader { 10 | 11 | 12 | override lateinit var downloadingInformation: DownloadingInformation 13 | private lateinit var itemDownloadManager: ItemDownloadManager 14 | 15 | override fun with(information: DownloadingInformation, itemDownloadManager: ItemDownloadManager): Downloader { 16 | this.downloadingInformation = information 17 | this.itemDownloadManager = itemDownloadManager 18 | return this 19 | } 20 | 21 | override fun download(): DownloadingItem = downloadingInformation.item 22 | override fun startDownload() = failDownload() 23 | override fun stopDownload() {} 24 | override fun finishDownload() {} 25 | override fun failDownload() = itemDownloadManager.removeACurrentDownload(downloadingInformation.item.id) 26 | override fun compatibility(downloadingInformation: DownloadingInformation) = -1 27 | override fun run() = startDownload() 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/selector/DownloaderSelector.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.manager.selector 2 | 3 | import com.github.davinkevin.podcastserver.manager.downloader.Downloader 4 | import com.github.davinkevin.podcastserver.manager.downloader.DownloadingInformation 5 | import com.github.davinkevin.podcastserver.manager.downloader.NoOpDownloader 6 | import org.springframework.aop.TargetClassAware 7 | import org.springframework.context.ApplicationContext 8 | import org.springframework.stereotype.Service 9 | 10 | /** 11 | * Created by kevin on 17/03/15. 12 | */ 13 | @Service 14 | class DownloaderSelector(val context: ApplicationContext, val downloaders: Set) { 15 | 16 | @Suppress("UNCHECKED_CAST") 17 | fun of(information: DownloadingInformation): Downloader { 18 | if (information.urls.isEmpty()) { 19 | return NO_OP_DOWNLOADER 20 | } 21 | 22 | val d = downloaders.minByOrNull { it.compatibility(information) }!! 23 | val clazz = (if (d is TargetClassAware) d.targetClass else d.javaClass) as Class 24 | return context.getBean(clazz) 25 | } 26 | 27 | companion object { 28 | val NO_OP_DOWNLOADER = NoOpDownloader() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/selector/UpdaterSelector.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.manager.selector 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.Type 4 | import com.github.davinkevin.podcastserver.update.updaters.Updater 5 | import java.net.URI 6 | 7 | /** 8 | * Created by kevin on 06/03/15. 9 | */ 10 | class UpdaterSelector(val updaters: Set) { 11 | fun of(url: URI): Updater = updaters.minByOrNull { updater: Updater -> updater.compatibility(url.toASCIIString()) }!! 12 | fun types(): Set = updaters.map { it.type() }.toSet() 13 | } 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/main/kotlin/com/github/davinkevin/podcastserver/messaging/MessagingTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.messaging 2 | 3 | import com.github.davinkevin.podcastserver.manager.downloader.DownloadingItem 4 | import org.springframework.context.ApplicationEventPublisher 5 | 6 | /** 7 | * Created by kevin on 2018-11-25 8 | */ 9 | class MessagingTemplate( 10 | private val event: ApplicationEventPublisher 11 | ) { 12 | fun sendWaitingQueue(value: List) { 13 | val v = WaitingQueueMessage(value) 14 | Thread.ofVirtual().start { event.publishEvent(v) } 15 | } 16 | fun sendItem(value: DownloadingItem) { 17 | val v = DownloadingItemMessage(value) 18 | Thread.ofVirtual().start { event.publishEvent(v) } 19 | } 20 | fun isUpdating(value: Boolean) { 21 | val v = UpdateMessage(value) 22 | Thread.ofVirtual().start { event.publishEvent(v) } 23 | } 24 | } 25 | 26 | sealed class Message(val topic: String, val value: T) 27 | class UpdateMessage(value: Boolean): Message("updating", value) 28 | class WaitingQueueMessage(value: List): Message>("waiting", value) 29 | class DownloadingItemMessage(value: DownloadingItem): Message("downloading", value) 30 | -------------------------------------------------------------------------------- /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(val id: UUID, val name: String, val items: Collection) { 15 | 16 | data class Item( 17 | val id: UUID, 18 | override val title: String, 19 | override val fileName: Path?, 20 | 21 | val description: String?, 22 | override val mimeType: String, 23 | val length: Long?, 24 | 25 | val pubDate: OffsetDateTime?, 26 | 27 | val podcast: Podcast, 28 | val cover: Cover 29 | ): Sluggable { 30 | 31 | data class Podcast(val id: UUID, val title: String) 32 | data class Cover (val id: UUID, val width: Int, val height: Int, val url: URI) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/playlist/PlaylistConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.playlist 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-07-01 10 | */ 11 | @Configuration 12 | class PlaylistRoutingConfig { 13 | 14 | @Bean 15 | fun playlistRouter(playlist: PlaylistHandler) = router { 16 | "/api/v1/playlists".nest { 17 | GET("", playlist::findAll) 18 | POST("", playlist::save) 19 | GET("{id}", playlist::findById) 20 | DELETE("{id}", playlist::deleteById) 21 | GET("{id}/rss", playlist::rss) 22 | "{id}/items/{itemId}".nest { 23 | POST("", playlist::addToPlaylist) 24 | DELETE("", playlist::removeFromPlaylist) 25 | } 26 | } 27 | } 28 | } 29 | 30 | @Configuration 31 | @Import( 32 | PlaylistRoutingConfig::class, 33 | PlaylistHandler::class, 34 | PlaylistService::class, 35 | PlaylistRepository::class 36 | ) 37 | class PlaylistConfig 38 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/playlist/PlaylistService.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.playlist 2 | 3 | import java.util.* 4 | 5 | class PlaylistService( 6 | private val repository: PlaylistRepository 7 | ) { 8 | 9 | fun findAll(): List = repository.findAll() 10 | fun findById(id: UUID): PlaylistWithItems? = repository.findById(id) 11 | fun save(name: String): PlaylistWithItems = repository.save(name) 12 | fun deleteById(id: UUID) = repository.deleteById(id) 13 | fun addToPlaylist(playlistId: UUID, itemId: UUID): PlaylistWithItems = 14 | repository.addToPlaylist(playlistId, itemId) 15 | fun removeFromPlaylist(playlistId: UUID, itemId: UUID): PlaylistWithItems = 16 | repository.removeFromPlaylist(playlistId, itemId) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /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.manager.selector.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 | -------------------------------------------------------------------------------- /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.manager.selector.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | val youtubedl: String = "/usr/local/bin/youtube-dl" 14 | ) 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/github/davinkevin/podcastserver/tag/TagHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.tag 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 | /** 9 | * Created by kevin on 2019-03-19 10 | */ 11 | class TagHandler(private val tagService: TagService) { 12 | 13 | fun findById(s: ServerRequest): ServerResponse { 14 | val id = s.pathVariable("id") 15 | .let(UUID::fromString) 16 | 17 | val tag = tagService.findById(id) 18 | ?: return ServerResponse.notFound().build() 19 | 20 | val body = TagHAL(tag.id, tag.name) 21 | 22 | return ServerResponse.ok().body(body) 23 | } 24 | 25 | fun findByNameLike(s: ServerRequest): ServerResponse { 26 | val name = s.paramOrNull("name") ?: "" 27 | 28 | val tags = tagService.findByNameLike(name) 29 | 30 | val body = tags 31 | .map { TagHAL(it.id, it.name) } 32 | .let(::TagsResponse) 33 | 34 | return ServerResponse.ok().body(body) 35 | } 36 | } 37 | 38 | private class TagHAL(val id: UUID, val name: String) 39 | private class TagsResponse(val content: Collection) 40 | -------------------------------------------------------------------------------- /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/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/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/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 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 | @Configuration 11 | @Import(ImageServiceConfig::class) 12 | class DailymotionUpdaterConfig { 13 | 14 | @Bean 15 | fun dailymotionUpdater( 16 | rcb: RestClient.Builder, 17 | image: ImageService 18 | ): DailymotionUpdater { 19 | val rc = rcb.clone().baseUrl("https://api.dailymotion.com").build() 20 | 21 | return DailymotionUpdater(rc, image) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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.fasterxml.jackson.databind.ObjectMapper 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 | 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 | mapper: ObjectMapper, 24 | clock: Clock 25 | ): FranceTvUpdater { 26 | val franceTvClient = rcb.clone().baseUrl("https://www.france.tv/").build() 27 | 28 | return FranceTvUpdater(franceTvClient, image, mapper, clock) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /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 org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.web.client.RestClient 8 | 9 | /** 10 | * Created by kevin on 14/03/2020 11 | */ 12 | @Configuration 13 | class GulliUpdaterConfig { 14 | 15 | @Bean 16 | fun gulliUpdater(rcb: RestClient.Builder, image: ImageService, mapper: ObjectMapper): GulliUpdater { 17 | val wc = rcb.clone() 18 | .baseUrl("https://replay.gulli.fr") 19 | .build() 20 | 21 | return GulliUpdater(wc, image, mapper) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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 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 | @Configuration 11 | @Import(ImageServiceConfig::class) 12 | class RSSUpdaterConfig { 13 | 14 | @Bean 15 | fun rssUpdater(imageService: ImageService, rcb: RestClient.Builder): RSSUpdater { 16 | return RSSUpdater(imageService, rcb.clone()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 java.net.URI 8 | 9 | class UploadUpdater : Updater { 10 | override fun findItems(podcast: PodcastToUpdate): List = emptyList() 11 | override fun signatureOf(url: URI): String = "" 12 | override fun type(): Type = TYPE 13 | override fun compatibility(url: String): Int = Integer.MAX_VALUE 14 | } 15 | 16 | private val TYPE = Type("upload", "Upload") 17 | -------------------------------------------------------------------------------- /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 org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | 6 | /** 7 | * Created by kevin on 01/05/2020 8 | */ 9 | @Configuration 10 | class UploadUpdaterConfig { 11 | 12 | @Bean 13 | fun uploadUpdater(): UploadUpdater = UploadUpdater() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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-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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/business/stats/NumberOfItemByDateWrapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.business.stats 2 | 3 | import com.github.davinkevin.podcastserver.podcast.NumberOfItemByDateWrapper 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import java.time.LocalDate 7 | import java.time.Month 8 | 9 | /** 10 | * Created by kevin on 01/07/15 for Podcast Server 11 | */ 12 | class NumberOfItemByDateWrapperTest { 13 | 14 | private val numberOfItemByDateWrapper = NumberOfItemByDateWrapper(LocalDate.of(2015, Month.JULY, 1), 100) 15 | 16 | @Test 17 | fun `should have the correct value`() { 18 | assertThat(numberOfItemByDateWrapper.date).isEqualTo(LocalDate.of(2015, Month.JULY, 1)) 19 | assertThat(numberOfItemByDateWrapper.numberOfItems).isEqualTo(100) 20 | } 21 | 22 | @Test 23 | fun `should be equals and have the same hashcode`() { 24 | /* Given */ 25 | val aCopy = NumberOfItemByDateWrapper(LocalDate.of(2015, Month.JULY, 1), 200) 26 | /* Then */ 27 | assertThat(numberOfItemByDateWrapper).isEqualTo(aCopy) 28 | assertThat(numberOfItemByDateWrapper.hashCode()).isEqualTo(aCopy.hashCode()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/worker/TypeTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.davinkevin.podcastserver.manager.worker 2 | 3 | import com.github.davinkevin.podcastserver.update.updaters.Type 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | 7 | /** 8 | * Created by kevin on 28/06/15 for Podcast Server 9 | */ 10 | class TypeTest { 11 | 12 | @Test 13 | fun should_have_key_and_name() { 14 | /* Given */ 15 | /* When */ 16 | val type = Type("Key", "Value") 17 | /* Then */ 18 | assertThat(type.key).isEqualTo("Key") 19 | assertThat(type.name).isEqualTo("Value") 20 | } 21 | 22 | @Test 23 | fun `should be equal if has same key and name`() { 24 | /* Given */ 25 | val k = "key" 26 | val n = "name" 27 | /* When */ 28 | val t1 = Type(k, n) 29 | val t2 = Type(k, n) 30 | /* Then */ 31 | assertThat(t1).isEqualTo(t2) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/resources/__files/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/backend/src/test/resources/__files/img/image.png -------------------------------------------------------------------------------- /backend/src/test/resources/__files/service/htmlService/jsoup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSOUP Example 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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/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/test/resources/__files/service/mimeTypeService/plain.text.txt: -------------------------------------------------------------------------------- 1 | plain.text -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/test/resources/__files/utils/multipart/outputfile.out: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | #junit.jupiter.testinstance.lifecycle.default = per_class -------------------------------------------------------------------------------- /backend/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /backend/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /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/resources/remote/downloader/rtmpdump/rtmpdump.txt: -------------------------------------------------------------------------------- 1 | Progression : (1%) 2 | Progression : (2%) 3 | Progression : (3%) 4 | Download Complete -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /backend/src/test/resources/spring.properties: -------------------------------------------------------------------------------- 1 | spring.test.enclosing.configuration=OVERRIDE 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /build-logic/build-plugin-database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-gradle-plugin` 3 | kotlin("jvm") version "2.0.0" 4 | } 5 | 6 | group = "com.gitlab.davinkevin.podcastserver.database" 7 | version = "2024.8.0" 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | gradlePlugin { 14 | plugins { 15 | create("DatabasePlugin") { 16 | id = "build-plugin-database" 17 | implementationClass = "com.gitlab.davinkevin.podcastserver.database.DatabasePlugin" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build-logic/build-plugin-docker-images/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-gradle-plugin` 3 | kotlin("jvm") version "2.0.0" 4 | } 5 | 6 | group = "com.gitlab.davinkevin.podcastserver.dockerimages" 7 | version = "2024.8.0" 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | gradlePlugin { 14 | plugins { 15 | create("DockerImagePlugin") { 16 | id = "build-plugin-docker-images" 17 | implementationClass = "com.gitlab.davinkevin.podcastserver.dockerimages.DockerImagePlugin" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /distribution/kpt/podcast-server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: "podcast-server" # {"$kpt-set":"namespace"} 4 | resources: 5 | - git::https://gitlab.com/davinkevin/Podcast-Server.git//distribution/kubernetes/base/ 6 | - ingress/ingress.yaml 7 | patchesStrategicMerge: 8 | - storage/backend.storage.yaml 9 | - storage/database.storage.yaml 10 | - storage/fs.storage.yaml 11 | configMapGenerator: 12 | - name: podcast-server 13 | behavior: merge 14 | literals: 15 | - "max-update-parallels=16" # {"$ref":"#/definitions/io.k8s.cli.substitutions.max-update-parallels"} 16 | - "concurrent-download=4" # {"$ref":"#/definitions/io.k8s.cli.substitutions.concurrent-download"} 17 | - "number-of-day-to-download=30" # {"$ref":"#/definitions/io.k8s.cli.substitutions.number-of-day-to-download"} 18 | - "number-of-day-to-save-cover=30" # {"$ref":"#/definitions/io.k8s.cli.substitutions.number-of-day-to-save-cover"} 19 | secretGenerator: 20 | - name: podcast-server 21 | behavior: merge 22 | literals: 23 | - database.password=TR8D=k`oXcrVJV=@zvtiqHy39F # {"$ref":"#/definitions/io.k8s.cli.substitutions.database.password"} 24 | - name: podcast-server-tls 25 | files: 26 | - ingress/tls.crt 27 | - ingress/tls.key 28 | type: kubernetes.io/tls 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /distribution/kubernetes/.infrastructure/contour/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - contour-gateway-provisioner.yaml -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | backendRefs: 13 | - name: backend 14 | port: 8080 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ingress/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: podcast-server 5 | spec: 6 | rules: 7 | - host: TO_BE_REPLACED 8 | http: 9 | paths: 10 | - path: /api 11 | pathType: Prefix 12 | backend: 13 | service: 14 | name: backend 15 | port: 16 | number: 8080 17 | - path: /actuator 18 | pathType: Prefix 19 | backend: 20 | service: 21 | name: backend 22 | port: 23 | number: 8080 24 | - path: /to-be-replace-by-bucket-name 25 | pathType: Prefix 26 | backend: 27 | service: 28 | name: storage 29 | port: 30 | number: 9000 31 | - pathType: Prefix 32 | path: /v2 33 | backend: 34 | service: 35 | name: ui-v2 36 | port: 37 | name: http 38 | - pathType: Prefix 39 | path: / 40 | backend: 41 | service: 42 | name: ui-v1 43 | port: 44 | name: http 45 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/ingress/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - ingress.yaml 6 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/namespace/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - namespace.yaml 6 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/components/namespace/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: podcast-server 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/gateway/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | resources: 5 | - storage.yaml 6 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /distribution/kubernetes/base/ui-v2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ui-v2 5 | labels: 6 | app: podcast-server 7 | module: frontend 8 | version: v2 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: v2 19 | serving: "true" 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: ui-v2 25 | spec: 26 | selector: 27 | matchLabels: 28 | app: podcast-server 29 | module: frontend 30 | version: v2 31 | serving: "true" 32 | template: 33 | metadata: 34 | labels: 35 | app: podcast-server 36 | module: frontend 37 | version: v2 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 | env: 53 | - name: PAGE_FALLBACK 54 | value: "/podcast-server/v2/index.html" 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | 2 | * xref:introduction.adoc[Introduction] 3 | 4 | * *Installation* 5 | 6 | ** xref:installation/kubernetes.adoc[on kubernetes] 7 | -------------------------------------------------------------------------------- /documentation/modules/ROOT/pages/introduction.adoc: -------------------------------------------------------------------------------- 1 | = Introduction 2 | 3 | This site is the complement documentation of the Podcast-Server project. 4 | -------------------------------------------------------------------------------- /documentation/supplemental-ui/partials/header-content.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fake-external-podcast/src/podcast/fake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/fake-external-podcast/src/podcast/fake.jpg -------------------------------------------------------------------------------- /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/.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /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/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 = "2024.8.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /frontend-angular/src/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $global-background-color: #fafafa; 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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-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 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcast/podcast.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ podcast.title }} 3 | 4 | 5 | 8 | 9 | 10 | rss_feed 11 | RSS Feed 12 | 13 | 17 | 18 | 22 | 26 | 27 | 28 | 29 | 30 |
31 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/app/podcasts/podcasts.component.html: -------------------------------------------------------------------------------- 1 | 2 | Podcasts 3 | 4 | 5 |
6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend-angular/src/app/podcasts/podcasts.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { MatIconModule, MatToolbarModule } from '@angular/material'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | import { EffectsModule } from '@ngrx/effects'; 6 | import { StoreModule } from '@ngrx/store'; 7 | 8 | import { SharedModule } from '../shared/shared.module'; 9 | 10 | import { PodcastsResolver } from './core/resolver/podcasts.resolver'; 11 | import { PodcastsComponent } from './podcasts.component'; 12 | import { PodcastsEffects } from './podcasts.effects'; 13 | import * as fromPodcasts from './podcasts.reducer'; 14 | 15 | const routes: Routes = [{ path: 'podcasts', component: PodcastsComponent, resolve: { podcasts: PodcastsResolver } }]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | SharedModule, 21 | 22 | /* Material Design */ 23 | MatToolbarModule, 24 | MatIconModule, 25 | 26 | /* Routes */ 27 | RouterModule.forChild(routes), 28 | 29 | /* NgRx */ 30 | StoreModule.forFeature('podcasts', fromPodcasts.reducer), 31 | EffectsModule.forFeature([PodcastsEffects]) 32 | ], 33 | providers: [PodcastsResolver], 34 | declarations: [PodcastsComponent] 35 | }) 36 | export class PodcastsModule {} 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend-angular/src/app/shared/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | dehaze 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend-angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/frontend-angular/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend-angular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | 5 | export const devTools = []; 6 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/frontend-angular/src/favicon.ico -------------------------------------------------------------------------------- /frontend-angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PodcastServer 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-angular/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /frontend-angularjs/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | ); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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-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 | }; -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/authorize-notification/authorize-notification.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/navbar/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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-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/www/app/common/component/updating/updating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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-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.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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/component/watchlist-chooser/watchlist-chooser.html: -------------------------------------------------------------------------------- 1 | 5 | 18 | -------------------------------------------------------------------------------- /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/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(' =""', '')); -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/modules/angularNotification.js: -------------------------------------------------------------------------------- 1 | import 'angular-notification'; 2 | 3 | export default { name : 'notification' }; -------------------------------------------------------------------------------- /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{} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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' }; -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /frontend-angularjs/www/app/common/service/playlistService.js: -------------------------------------------------------------------------------- 1 | import {Module, Service} from '../../decorators'; 2 | import NgStorage from 'ngstorage'; 3 | 4 | @Module({ 5 | name : 'ps.common.service.playlist', 6 | modules : [ NgStorage ] 7 | }) 8 | @Service('playlistService') 9 | export default class PlaylistService { 10 | 11 | constructor($localStorage) { 12 | "ngInject"; 13 | this.$localStorage = $localStorage; 14 | this.$localStorage.playlist = this.$localStorage.playlist || []; 15 | } 16 | 17 | playlist() { 18 | return this.$localStorage.playlist; 19 | } 20 | isEmpty() { 21 | return this.playlist().length === 0; 22 | } 23 | play(item) { 24 | this.removeAll(); 25 | this.add(item); 26 | } 27 | add(item) { 28 | this.playlist().push(item); 29 | } 30 | remove (item) { 31 | this.$localStorage.playlist = this.playlist().filter(elem => elem.id !== item.id); 32 | } 33 | contains(item) { 34 | return !!this.playlist().find(elem => elem.id === item.id); 35 | } 36 | addOrRemove(item) { 37 | this.contains(item) ? this.remove(item) : this.add(item); 38 | } 39 | removeAll () { 40 | this.$localStorage.playlist = []; 41 | } 42 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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{} -------------------------------------------------------------------------------- /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/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-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!'; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/'; -------------------------------------------------------------------------------- /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 {} -------------------------------------------------------------------------------- /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 {} -------------------------------------------------------------------------------- /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/config/styles/styles.js: -------------------------------------------------------------------------------- 1 | import './bootstrap-adaptation.css!'; 2 | import './podcastserver.css!'; 3 | import './tags-input-bootstrap.css!'; -------------------------------------------------------------------------------- /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{} -------------------------------------------------------------------------------- /frontend-angularjs/www/app/podcasts/details/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | - 5 | 6 | + 7 |
8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/podcasts/details/upload/upload.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
7 |
8 | Drop one or many files here 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend-angularjs/www/app/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 7 |
8 | 9 |
10 | - 11 | 12 | + 13 |
14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.jvm.target.validation.mode = IGNORE 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davinkevin/Podcast-Server/7f4272b889325a1689c9dd10c8bf43fbd9a2beb5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v4beta8 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: podcastserver/backend-base-image 6 | docker: 7 | dockerfile: backend/src/main/base-image/Dockerfile 8 | - image: podcastserver/backend 9 | jib: 10 | project: backend 11 | args: 12 | - -Pskaffold=true 13 | fromImage: podcastserver/backend-base-image 14 | requires: 15 | - image: podcastserver/backend-base-image 16 | - image: podcastserver/init-db 17 | docker: 18 | dockerfile: ./backend-lib-database/src/main/docker/Dockerfile 19 | - image: podcastserver/storage 20 | context: storage/ 21 | - image: podcastserver/ui 22 | custom: 23 | buildCommand: | 24 | set -euo pipefail 25 | sh ui/compile.sh 26 | docker build -f ui/target/docker/Dockerfile ui/target/docker/ -t $IMAGE 27 | if $PUSH_IMAGE; then 28 | docker push $IMAGE 29 | fi 30 | dependencies: 31 | paths: 32 | - frontend-angularjs/target/dist 33 | - frontend-angular/dist 34 | - ui 35 | manifests: 36 | kustomize: 37 | paths: 38 | - distribution/kubernetes/overlays/podcast.k8s.local/ 39 | deploy: 40 | kubectl: {} 41 | -------------------------------------------------------------------------------- /storage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM minio/minio:RELEASE.2024-08-03T04-33-23Z 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ui/src/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM joseluisq/static-web-server:2.32.1 2 | 3 | COPY podcast-server /podcast-server 4 | COPY config.toml / 5 | 6 | ENV SERVER_CONFIG_FILE=config.toml 7 | -------------------------------------------------------------------------------- /ui/src/docker/config.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | host = "::" 4 | port = 8080 5 | root = "/podcast-server" 6 | log-level = "info" 7 | 8 | page-fallback = "/podcast-server/index.html" 9 | cache-control-headers = true 10 | compression = true 11 | security-headers = true 12 | directory-listing = false 13 | redirect-trailing-slash = true 14 | compression-static = true 15 | ignore-hidden-files = true 16 | --------------------------------------------------------------------------------