From dfe4e75826d08ff780672e0c5560d883de15ff93 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 15 Apr 2026 19:34:51 -0300 Subject: [PATCH 01/11] Replace "Lyrics" naming with more generic "TextTracks" --- ...ricsController.java => TextTracksController.java} | 10 +++++----- ...nterface.java => TextTracksServiceInterface.java} | 2 +- .../{LyricsService.java => TextTracksService.java} | 5 ++--- ...rollerTest.java => TextTracksControllerTest.java} | 12 ++++++------ 4 files changed, 14 insertions(+), 15 deletions(-) rename src/main/java/cc/wordview/api/controller/{LyricsController.java => TextTracksController.java} (89%) rename src/main/java/cc/wordview/api/service/{LyricsServiceInterface.java => TextTracksServiceInterface.java} (98%) rename src/main/java/cc/wordview/api/service/implementation/{LyricsService.java => TextTracksService.java} (95%) rename src/test/java/cc/wordview/api/test/api/controller/{LyricsControllerTest.java => TextTracksControllerTest.java} (83%) diff --git a/src/main/java/cc/wordview/api/controller/LyricsController.java b/src/main/java/cc/wordview/api/controller/TextTracksController.java similarity index 89% rename from src/main/java/cc/wordview/api/controller/LyricsController.java rename to src/main/java/cc/wordview/api/controller/TextTracksController.java index 9d00ec9..6f0ef69 100644 --- a/src/main/java/cc/wordview/api/controller/LyricsController.java +++ b/src/main/java/cc/wordview/api/controller/TextTracksController.java @@ -19,7 +19,7 @@ import cc.wordview.api.Application; import cc.wordview.api.response.LyricsResponse; -import cc.wordview.api.service.implementation.LyricsService; +import cc.wordview.api.service.implementation.TextTracksService; import cc.wordview.api.service.VideoLyricsServiceInterface; import cc.wordview.api.util.ArrayUtil; import cc.wordview.api.runtime.ResourceResolver; @@ -40,15 +40,15 @@ @RestController @CrossOrigin(origins = Application.CORS_ORIGIN) -@RequestMapping(path = Application.API_PATH + "/lyrics") -public class LyricsController extends ServiceController { +@RequestMapping(path = Application.API_PATH + "/text-tracks") +public class TextTracksController extends ServiceController { @Autowired private ResourceResolver resourceResolver; @Autowired private VideoLyricsServiceInterface videoLyricsService; - @GetMapping(produces = "application/json;charset=utf-8") + @GetMapping(produces = "application/json;charset=utf-8", path = "/lyrics") public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String lang, @RequestParam String trackName, @RequestParam String artistName) throws IOException, LyricsNotFoundException, LanguageNotFoundException { String decodedTrackName = URLDecoder.decode(trackName); String decodedArtistName = URLDecoder.decode(artistName); @@ -64,7 +64,7 @@ public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String return ok(new LyricsResponse(lyrics, words)); } - @GetMapping(produces = "application/json;charset=utf-8", path = "/list") + @GetMapping(produces = "application/json;charset=utf-8", path = "/lyrics/list") public ResponseEntity getLyricsList() { ArrayList ids = videoLyricsService.listLyricsIds(); return ok(ids); diff --git a/src/main/java/cc/wordview/api/service/LyricsServiceInterface.java b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java similarity index 98% rename from src/main/java/cc/wordview/api/service/LyricsServiceInterface.java rename to src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java index be5790a..f35f340 100644 --- a/src/main/java/cc/wordview/api/service/LyricsServiceInterface.java +++ b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java @@ -22,7 +22,7 @@ import java.io.IOException; -public interface LyricsServiceInterface { +public interface TextTracksServiceInterface { /** * Retrieves the lyrics for a track * diff --git a/src/main/java/cc/wordview/api/service/implementation/LyricsService.java b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java similarity index 95% rename from src/main/java/cc/wordview/api/service/implementation/LyricsService.java rename to src/main/java/cc/wordview/api/service/implementation/TextTracksService.java index e31f790..dbaa406 100644 --- a/src/main/java/cc/wordview/api/service/implementation/LyricsService.java +++ b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java @@ -18,8 +18,7 @@ package cc.wordview.api.service.implementation; import cc.wordview.api.runtime.cache.LyricsCache; -import cc.wordview.api.service.LyricsServiceInterface; -import cc.wordview.api.runtime.DownloaderImpl; +import cc.wordview.api.service.TextTracksServiceInterface; import cc.wordview.wordfind.WordFind; import cc.wordview.wordfind.exception.LyricsNotFoundException; import jakarta.annotation.PostConstruct; @@ -36,7 +35,7 @@ import java.util.Objects; @Service -public class LyricsService implements LyricsServiceInterface { +public class TextTracksService implements TextTracksServiceInterface { private static StreamingService YTService; private final WordFind client = new WordFind(); diff --git a/src/test/java/cc/wordview/api/test/api/controller/LyricsControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java similarity index 83% rename from src/test/java/cc/wordview/api/test/api/controller/LyricsControllerTest.java rename to src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java index 0a35923..2575444 100644 --- a/src/test/java/cc/wordview/api/test/api/controller/LyricsControllerTest.java +++ b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java @@ -26,11 +26,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -class LyricsControllerTest extends ControllerTest { +class TextTracksControllerTest extends ControllerTest { @Test @Disabled("Makes a external request, only run this when ABSOLUTELY NECESSARY") void getLyrics() throws Exception { - req.get("/lyrics?id=1cGQotpn8r4&lang=ja&trackName=a&artistName=a") + req.get("/text-tracks/lyrics?id=1cGQotpn8r4&lang=ja&trackName=a&artistName=a") .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=utf-8")); } @@ -38,7 +38,7 @@ void getLyrics() throws Exception { @Test // @Disabled("Does not make external request but don't seem to work on CI") void getLyricsWordView() throws Exception { - req.get("/lyrics?id=ZnUEeXpxBJ0&lang=pt&trackName=aquarela&artistName=toquinho") + req.get("/text-tracks/lyrics?id=ZnUEeXpxBJ0&lang=pt&trackName=aquarela&artistName=toquinho") .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=utf-8")); } @@ -46,7 +46,7 @@ void getLyricsWordView() throws Exception { @Test @Disabled("Makes a external request, only run this when ABSOLUTELY NECESSARY") void getLyricsWordFind() throws Exception { - req.get("/lyrics?id=vcw5THyM7Jo&lang=ja&trackName=%s&artistName=%s".formatted( + req.get("/text-tracks/lyrics?id=vcw5THyM7Jo&lang=ja&trackName=%s&artistName=%s".formatted( URLEncoder.encode("終点の先が在るとするならば。"), URLEncoder.encode("ツユ") )) @@ -58,7 +58,7 @@ void getLyricsWordFind() throws Exception { @Test @Disabled("Makes a external request, only run this when ABSOLUTELY NECESSARY") void getLyricsNotFound() throws Exception { - req.get("/lyrics?id=vcw5THyM7Jo&lang=ja&trackName=%s&artistName=%s".formatted( + req.get("/text-tracks/lyrics?id=vcw5THyM7Jo&lang=ja&trackName=%s&artistName=%s".formatted( URLEncoder.encode("a_song_that_doesnt_exist"), URLEncoder.encode("aa") )).andExpect(status().isNotFound()); @@ -66,7 +66,7 @@ void getLyricsNotFound() throws Exception { @Test void getListOfLyricsIds() throws Exception { - req.get("/lyrics/list") + req.get("/text-tracks/lyrics/list") .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=utf-8")); } From aee1673ac3afabf2a98df307f7fbf00ed806dc95 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Thu, 16 Apr 2026 12:30:32 -0300 Subject: [PATCH 02/11] Update NewPipeExtractor --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fe2a4a3..76156b0 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ com.github.TeamNewPipe NewPipeExtractor - v0.24.6 + v0.26.1 com.squareup.okhttp3 From 09cc906d3abc220ac610f0ed0e8d341b28329e16 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Thu, 16 Apr 2026 12:41:11 -0300 Subject: [PATCH 03/11] Implement video caption retrieval Now the api returns video subtitles alongside with lyrics --- .../api/controller/TextTracksController.java | 22 ++++- .../api/database/entity/VideoSubtitles.java | 46 +++++++++++ .../exception/SubtitleNotFoundException.java | 24 ++++++ .../repository/VideoSubtitlesRepository.java | 27 ++++++ ...csResponse.java => TextTrackResponse.java} | 4 +- .../api/runtime/ResourceResolver.java | 10 ++- .../api/runtime/cache/SubtitlesCache.java | 82 +++++++++++++++++++ .../service/TextTracksServiceInterface.java | 12 +++ .../VideoSubtitlesServiceInterface.java | 32 ++++++++ .../implementation/TextTracksService.java | 42 +++++++++- .../implementation/VideoSubtitlesService.java | 74 +++++++++++++++++ src/main/resources/application-dev.properties | 1 + .../resources/application-prod.properties | 1 + src/main/resources/data.sql | 36 ++++---- src/main/resources/schema.sql | 43 ++++++---- .../subtitles/uma_wo_kakudaisuru_ge-mu.vtt | 8 ++ .../controller/TextTracksControllerTest.java | 17 +++- 17 files changed, 439 insertions(+), 42 deletions(-) create mode 100644 src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java create mode 100644 src/main/java/cc/wordview/api/exception/SubtitleNotFoundException.java create mode 100644 src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java rename src/main/java/cc/wordview/api/response/{LyricsResponse.java => TextTrackResponse.java} (93%) create mode 100644 src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java create mode 100644 src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java create mode 100644 src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java create mode 100644 src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt diff --git a/src/main/java/cc/wordview/api/controller/TextTracksController.java b/src/main/java/cc/wordview/api/controller/TextTracksController.java index 6f0ef69..b41677c 100644 --- a/src/main/java/cc/wordview/api/controller/TextTracksController.java +++ b/src/main/java/cc/wordview/api/controller/TextTracksController.java @@ -18,16 +18,17 @@ package cc.wordview.api.controller; import cc.wordview.api.Application; -import cc.wordview.api.response.LyricsResponse; -import cc.wordview.api.service.implementation.TextTracksService; +import cc.wordview.api.response.TextTrackResponse; +import cc.wordview.api.runtime.ResourceResolver; import cc.wordview.api.service.VideoLyricsServiceInterface; +import cc.wordview.api.service.implementation.TextTracksService; import cc.wordview.api.util.ArrayUtil; -import cc.wordview.api.runtime.ResourceResolver; import cc.wordview.gengolex.Language; import cc.wordview.gengolex.LanguageNotFoundException; import cc.wordview.gengolex.Parser; import cc.wordview.gengolex.word.Word; import cc.wordview.wordfind.exception.LyricsNotFoundException; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -61,7 +62,7 @@ public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String ArrayList words = ArrayUtil.withoutDuplicates(parser.findWords(lyrics.replace("\n", " "))); - return ok(new LyricsResponse(lyrics, words)); + return ok(new TextTrackResponse(lyrics, words)); } @GetMapping(produces = "application/json;charset=utf-8", path = "/lyrics/list") @@ -69,4 +70,17 @@ public ResponseEntity getLyricsList() { ArrayList ids = videoLyricsService.listLyricsIds(); return ok(ids); } + + @GetMapping(produces = "application/json;charset=utf-8", path = "/subtitles") + public ResponseEntity getSubtitle(@RequestParam String id, @RequestParam String lang) throws ExtractionException, IOException, LanguageNotFoundException { + String subtitle = service.getSubtitle(id, lang); + + String dictionariesPath = resourceResolver.getDictionariesPath(); + + Parser parser = new Parser(Language.Companion.byTag(lang), dictionariesPath); + + ArrayList words = ArrayUtil.withoutDuplicates(parser.findWords(subtitle.replace("\n", " "))); + + return ok(new TextTrackResponse(subtitle, words)); + } } diff --git a/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java b/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java new file mode 100644 index 0000000..6a696f3 --- /dev/null +++ b/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.database.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Entity +@Data +@Table(name = "video_subtitles") +public class VideoSubtitles implements Serializable { + @Serial + private static final long serialVersionUID = 5354932348244429655L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "video_id", unique = true) + private String videoId; + + @Column(name = "subtitle_file") + private String subtitleFile; + + @Column(name = "time_offset") + private int offset; +} diff --git a/src/main/java/cc/wordview/api/exception/SubtitleNotFoundException.java b/src/main/java/cc/wordview/api/exception/SubtitleNotFoundException.java new file mode 100644 index 0000000..8afb110 --- /dev/null +++ b/src/main/java/cc/wordview/api/exception/SubtitleNotFoundException.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.exception; + +public class SubtitleNotFoundException extends RuntimeException { + public SubtitleNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java b/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java new file mode 100644 index 0000000..7f5dada --- /dev/null +++ b/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.repository; + +import cc.wordview.api.database.entity.VideoSubtitles; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface VideoSubtitlesRepository extends CrudRepository { + Optional findByVideoId(String videoId); +} diff --git a/src/main/java/cc/wordview/api/response/LyricsResponse.java b/src/main/java/cc/wordview/api/response/TextTrackResponse.java similarity index 93% rename from src/main/java/cc/wordview/api/response/LyricsResponse.java rename to src/main/java/cc/wordview/api/response/TextTrackResponse.java index ba292f9..cf9c849 100644 --- a/src/main/java/cc/wordview/api/response/LyricsResponse.java +++ b/src/main/java/cc/wordview/api/response/TextTrackResponse.java @@ -25,7 +25,7 @@ @Data @AllArgsConstructor -public class LyricsResponse { - private String lyrics; +public class TextTrackResponse { + private String textTrack; private List dictionary; } diff --git a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java index bcbfe56..9ac056c 100644 --- a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java +++ b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java @@ -43,6 +43,9 @@ public class ResourceResolver { @Value("${wordview.lyrics_path}") private String lyricsPath; + @Value("${wordview.subtitles_path}") + private String subtitlesPath; + @Value("${wordview.phrases_path}") private String phrasesPath; @@ -59,11 +62,12 @@ public void debugShowPaths() { Dictionaries Path: {} Images Path: {} Lyrics Path: {} + Subtitles Path: {} Phrases Path: {} Translations Path: {} Feeds Path: {} """, - dictionariesPath, imagesPath, lyricsPath, phrasesPath, translationsPath, feedsPath); + dictionariesPath, imagesPath, lyricsPath, subtitlesPath, phrasesPath, translationsPath, feedsPath); } public String getDictionariesPath() throws IOException { @@ -78,6 +82,10 @@ public String getLyricsPath() throws IOException { return resolvePathOrClasspath(lyricsPath); } + public String getSubtitlesPath() throws IOException { + return resolvePathOrClasspath(subtitlesPath); + } + public String getPhrasesPath() throws IOException { return resolvePathOrClasspath(phrasesPath); } diff --git a/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java b/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java new file mode 100644 index 0000000..129a140 --- /dev/null +++ b/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.runtime.cache; + +import cc.wordview.api.database.entity.VideoSubtitles; +import cc.wordview.api.runtime.ResourceResolver; +import cc.wordview.api.service.VideoSubtitlesServiceInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +@Component +public class SubtitlesCache extends HashMapCacheManager { + private static final Logger logger = LoggerFactory.getLogger(SubtitlesCache.class); + + @Autowired + private ResourceResolver resourceResolver; + + @Autowired + private VideoSubtitlesServiceInterface videoSubtitlesService; + + @Override + public void init() throws IOException { + String subtitlesPath = resourceResolver.getSubtitlesPath(); + Path filepath = Path.of(subtitlesPath); + + Map fileToContent = new HashMap<>(); + + try (Stream paths = Files.walk(filepath)) { + paths.filter(Files::isRegularFile).forEach(file -> { + try { + fileToContent.put(file.getFileName().toString(), Files.readString(file)); + } catch (IOException e) { + logger.error("Failed to read subtitle file, ignoring this file", e); + } + }); + + List videoSubtitles = videoSubtitlesService.getAll(); + + for (var subtitle : videoSubtitles) { + String content = fileToContent.get(subtitle.getSubtitleFile() + ".vtt"); + map.put(subtitle.getVideoId(), content); + } + + logger.info("Preloaded {} subtitles", map.size()); + } catch (IOException e) { + logger.error("Failed to read the subtitles path", e); + } + } + + public void put(String id, String subtitle) { + if (map.containsKey(id)) + return; + + logger.info("Adding {} to cache, there are now {} subtitles", id, map.size() + 1); + map.put(id, subtitle); + } +} diff --git a/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java index f35f340..1d6fd88 100644 --- a/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java +++ b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java @@ -17,6 +17,7 @@ package cc.wordview.api.service; +import cc.wordview.api.exception.SubtitleNotFoundException; import cc.wordview.wordfind.exception.LyricsNotFoundException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -37,6 +38,17 @@ public interface TextTracksServiceInterface { */ String getLyrics(String id, String trackName, String artistName, String langTag) throws ExtractionException, IOException, LyricsNotFoundException; + /** + * Retrieves the subtitles for the track given the `id` + * @param id the YouTube id of the video. + * @param langTag the language the subtitle should be in. + * @return the subtitles formatted in VTT. + * @throws ExtractionException if subtitle extraction from YouTube fails. + * @throws IOException if reading subtitles from disk fails. + * @throws SubtitleNotFoundException if no valid subtitles are found. + */ + String getSubtitle(String id, String langTag) throws ExtractionException, IOException, SubtitleNotFoundException; + /** * Attempts to retrieve lyrics for the given track and artist from an external provider from WordFind. * diff --git a/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java b/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java new file mode 100644 index 0000000..b8b18e5 --- /dev/null +++ b/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.service; + +import cc.wordview.api.database.entity.VideoSubtitles; +import cc.wordview.api.exception.NoSuchEntryException; + +import java.util.ArrayList; +import java.util.List; + +public interface VideoSubtitlesServiceInterface extends ServiceInterface { + VideoSubtitles getByVideoId(String videoId) throws NoSuchEntryException; + + ArrayList listLyricsIds(); + + List getAll(); +} diff --git a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java index dbaa406..b56cc29 100644 --- a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java +++ b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java @@ -17,7 +17,9 @@ package cc.wordview.api.service.implementation; +import cc.wordview.api.exception.SubtitleNotFoundException; import cc.wordview.api.runtime.cache.LyricsCache; +import cc.wordview.api.runtime.cache.SubtitlesCache; import cc.wordview.api.service.TextTracksServiceInterface; import cc.wordview.wordfind.WordFind; import cc.wordview.wordfind.exception.LyricsNotFoundException; @@ -41,11 +43,14 @@ public class TextTracksService implements TextTracksServiceInterface { private final WordFind client = new WordFind(); @Autowired - private LyricsCache cache; + private LyricsCache lyricsCache; + + @Autowired + private SubtitlesCache subtitlesCache; @Override public String getLyrics(String id, String trackName, String artistName, String langTag) throws IOException, LyricsNotFoundException { - String lyrics = cache.get(id); + String lyrics = lyricsCache.get(id); if (lyrics == null) lyrics = getLyricsYT(id, langTag); @@ -58,10 +63,38 @@ public String getLyrics(String id, String trackName, String artistName, String l throw new LyricsNotFoundException("Unable to find lyrics for %s".formatted(trackName)); } - cache.put(id, lyrics); + lyricsCache.put(id, lyrics); return lyrics; } + @Override + public String getSubtitle(String id, String langTag) throws ExtractionException, IOException, SubtitleNotFoundException { + String subtitle = subtitlesCache.get(id); + + if (subtitle == null) { + StreamInfo info = StreamInfo.getInfo(YTService, "https://youtube.com/watch?v=" + id); + + for (SubtitlesStream st : info.getSubtitles()) { + if (st.isAutoGenerated()) continue; + + if (Objects.equals(st.getLanguageTag(), langTag)) { + String url = st.getContent().replace("&fmt=ttml", "&fmt=vtt"); + + RestTemplate restTemplate = new RestTemplate(); + subtitle = restTemplate.getForObject(url, String.class); + } + } + } + + if (subtitle == null) { + throw new SubtitleNotFoundException("Unable to find any subtitles for the video %s".formatted(id)); + } + + subtitlesCache.put(id, subtitle); + + return subtitle; + } + private String getLyricsYT(String id, String langTag) { StreamInfo info; @@ -92,7 +125,8 @@ private void initNewPipe() throws ExtractionException { @PostConstruct private void preloadLyrics() throws IOException { - cache.init(); + lyricsCache.init(); + subtitlesCache.init(); } @Override diff --git a/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java b/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java new file mode 100644 index 0000000..4363e25 --- /dev/null +++ b/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.service.implementation; + +import cc.wordview.api.database.entity.VideoSubtitles; +import cc.wordview.api.exception.NoSuchEntryException; +import cc.wordview.api.repository.VideoSubtitlesRepository; +import cc.wordview.api.service.VideoSubtitlesServiceInterface; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class VideoSubtitlesService implements VideoSubtitlesServiceInterface { + @Autowired + private VideoSubtitlesRepository repository; + + + @Override + public VideoSubtitles getByVideoId(String videoId) throws NoSuchEntryException { + Optional videoSubtitle = repository.findByVideoId(videoId); + + if (!videoSubtitle.isPresent()) { + throw new NoSuchEntryException("Unable to find any subtitles with this videoId"); + } + + return videoSubtitle.get(); + } + + @Override + public ArrayList listLyricsIds() { + Iterable allSubtitles = repository.findAll(); + ArrayList ids = new ArrayList<>(); + + for (VideoSubtitles subtitle : allSubtitles) { + ids.add(subtitle.getVideoId()); + } + + return ids; + } + + @Override + public List getAll() { + return (List) repository.findAll(); + } + + @Override + public VideoSubtitles getById(Long id) throws NoSuchEntryException { + throw new UnsupportedOperationException("getById is disabled for VideoSubtitlesService"); + } + + @Override + public VideoSubtitles insert(VideoSubtitles entity) throws Exception { + throw new UnsupportedOperationException("insert is disabled for VideoSubtitlesService"); + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index b7415a5..8ba91a5 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -18,6 +18,7 @@ spring.jpa.open-in-view=false wordview.dictionaries_path=classpath:/dictionaries/ wordview.images_path=classpath:/images/ wordview.lyrics_path=classpath:/lyrics/ +wordview.subtitles_path=classpath:/subtitles/ wordview.phrases_path=classpath:/phrases/ wordview.translations_path=classpath:/translations/ wordview.feeds_path=classpath:/feeds/ diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index ee92e76..14d7b57 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -17,5 +17,6 @@ spring.jpa.open-in-view=false wordview.dictionaries_path= wordview.images_path= wordview.lyrics_path= +wordview.subtitles_path= wordview.phrases_path= wordview.feeds_path= \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 8bd4a0b..f5a60b6 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,23 +1,29 @@ -INSERT INTO member (username, email, password) VALUES - ('Arthur', 'arthur.araujo@tutanota.com', 'senha'); +INSERT INTO member (username, email, password) +VALUES ('Arthur', 'arthur.araujo@tutanota.com', 'senha'); -INSERT INTO member (username, email, password) VALUES - ('conta', 'conta2@tutanota.com', 'senha'); +INSERT INTO member (username, email, password) +VALUES ('conta', 'conta2@tutanota.com', 'senha'); -INSERT INTO member (username, email, password, role) VALUES - ('mock.user', 'mock.user@gmail.com', '8631b7298388ab36f3785770cc07c4ea', 'USER'); +INSERT INTO member (username, email, password, role) +VALUES ('mock.user', 'mock.user@gmail.com', '8631b7298388ab36f3785770cc07c4ea', 'USER'); -INSERT INTO member (username, email, password, role) VALUES - ('mock.disposable', 'mock.disposable@gmail.com', '5b4a4769301f10a5ac64609e7e0bba0e', 'USER'); +INSERT INTO member (username, email, password, role) +VALUES ('mock.disposable', 'mock.disposable@gmail.com', '5b4a4769301f10a5ac64609e7e0bba0e', 'USER'); -INSERT INTO member (username, email, password, role) VALUES - ('mock.disposable.email', 'mock.disposable.email@gmail.com', '407c8797b415f2e557dc141d16c4ac86', 'USER'); +INSERT INTO member (username, email, password, role) +VALUES ('mock.disposable.email', 'mock.disposable.email@gmail.com', '407c8797b415f2e557dc141d16c4ac86', 'USER'); -INSERT INTO member (username, email, password, role) VALUES - ('mock.admin', 'mock.admin@gmail.com', '061cf224cffe1951e32ffaa1c414544a', 'ADMIN'); +INSERT INTO member (username, email, password, role) +VALUES ('mock.admin', 'mock.admin@gmail.com', '061cf224cffe1951e32ffaa1c414544a', 'ADMIN'); -INSERT INTO email (email) VALUES ('mock.user@gmail.com'); +INSERT INTO email (email) +VALUES ('mock.user@gmail.com'); -INSERT INTO known_words (user_id, lang, words) VALUES (3, 'en', 'rain,world'); +INSERT INTO known_words (user_id, lang, words) +VALUES (3, 'en', 'rain,world'); -INSERT INTO video_lyrics (video_id, lyrics_file, time_offset) VALUES ('ZnUEeXpxBJ0', 'aquarela', 0); \ No newline at end of file +INSERT INTO video_lyrics (video_id, lyrics_file, time_offset) +VALUES ('ZnUEeXpxBJ0', 'aquarela', 0); + +INSERT INTO video_subtitles (video_id, subtitle_file, time_offset) +VALUES ('K9Ydi94yT94', 'uma_wo_kakudaisuru_ge-mu', 0); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 2660a16..232c5c3 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -2,34 +2,47 @@ DROP TABLE IF EXISTS member CASCADE; DROP TABLE IF EXISTS email CASCADE; DROP TABLE IF EXISTS video_lyrics CASCADE; -CREATE TABLE member ( - id bigint GENERATED ALWAYS AS IDENTITY, - username varchar(255), - email varchar(255) UNIQUE, - password varchar(255), - role varchar(255), +CREATE TABLE member +( + id bigint GENERATED ALWAYS AS IDENTITY, + username varchar(255), + email varchar(255) UNIQUE, + password varchar(255), + role varchar(255), lesson_time bigint DEFAULT 300000, PRIMARY KEY (id) ); -CREATE TABLE email ( - id bigint GENERATED ALWAYS AS IDENTITY, +CREATE TABLE email +( + id bigint GENERATED ALWAYS AS IDENTITY, email varchar(255) UNIQUE, PRIMARY KEY (id) ); -CREATE TABLE video_lyrics ( - id bigint GENERATED ALWAYS AS IDENTITY, - video_id varchar(12) UNIQUE, +CREATE TABLE video_lyrics +( + id bigint GENERATED ALWAYS AS IDENTITY, + video_id varchar(12) UNIQUE, lyrics_file varchar(128), time_offset int, PRIMARY KEY (id) ); -CREATE TABLE known_words ( - id bigint GENERATED ALWAYS AS IDENTITY, +CREATE TABLE video_subtitles +( + id bigint GENERATED ALWAYS AS IDENTITY, + video_id varchar(12) UNIQUE, + subtitle_file varchar(128), + time_offset int, + PRIMARY KEY (id) +); + +CREATE TABLE known_words +( + id bigint GENERATED ALWAYS AS IDENTITY, user_id bigint, - lang varchar(64), - words varchar(5120), + lang varchar(64), + words varchar(5120), PRIMARY KEY (id) ); diff --git a/src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt b/src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt new file mode 100644 index 0000000..60c772e --- /dev/null +++ b/src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt @@ -0,0 +1,8 @@ +WEBVTT + +00:00:0.100 --> 00:00:3.000 +馬を拡大して面白い形にしたいなって + +00:00:3.000 --> 00:00:4.500 +思ったことはありますか? + diff --git a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java index 2575444..b9ca380 100644 --- a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java +++ b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java @@ -36,7 +36,7 @@ void getLyrics() throws Exception { } @Test -// @Disabled("Does not make external request but don't seem to work on CI") + @Disabled("Does not make external request but don't seem to work on CI") void getLyricsWordView() throws Exception { req.get("/text-tracks/lyrics?id=ZnUEeXpxBJ0&lang=pt&trackName=aquarela&artistName=toquinho") .andExpect(status().isOk()) @@ -70,5 +70,20 @@ void getListOfLyricsIds() throws Exception { .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=utf-8")); } + + @Test + @Disabled("Makes a external request, only run this when ABSOLUTELY NECESSARY") + void getSubtitlesYouTube() throws Exception { + req.get("/text-tracks/subtitles?id=eCipMjo1vSI&lang=ja") + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=utf-8")); + } + + @Test + void getSubtitlesWordView() throws Exception { + req.get("/text-tracks/subtitles?id=K9Ydi94yT94&lang=ja") + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=utf-8")); + } } From 49775363eec54667ee07ce50a6169e30c73750d0 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Thu, 16 Apr 2026 13:16:15 -0300 Subject: [PATCH 04/11] Remove unused method declarations --- .../service/VideoLyricsServiceInterface.java | 3 --- .../VideoSubtitlesServiceInterface.java | 6 ----- .../implementation/VideoLyricsService.java | 13 ---------- .../implementation/VideoSubtitlesService.java | 25 ------------------- 4 files changed, 47 deletions(-) diff --git a/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java b/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java index 3a5cdf0..c1842da 100644 --- a/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java +++ b/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java @@ -18,14 +18,11 @@ package cc.wordview.api.service; import cc.wordview.api.database.entity.VideoLyrics; -import cc.wordview.api.exception.NoSuchEntryException; import java.util.ArrayList; import java.util.List; public interface VideoLyricsServiceInterface extends ServiceInterface { - VideoLyrics getByVideoId(String videoId) throws NoSuchEntryException; - ArrayList listLyricsIds(); List getAll(); diff --git a/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java b/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java index b8b18e5..364d9f8 100644 --- a/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java +++ b/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java @@ -18,15 +18,9 @@ package cc.wordview.api.service; import cc.wordview.api.database.entity.VideoSubtitles; -import cc.wordview.api.exception.NoSuchEntryException; -import java.util.ArrayList; import java.util.List; public interface VideoSubtitlesServiceInterface extends ServiceInterface { - VideoSubtitles getByVideoId(String videoId) throws NoSuchEntryException; - - ArrayList listLyricsIds(); - List getAll(); } diff --git a/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java b/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java index 6af2416..095ab50 100644 --- a/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java +++ b/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java @@ -18,7 +18,6 @@ package cc.wordview.api.service.implementation; import cc.wordview.api.database.entity.VideoLyrics; -import cc.wordview.api.exception.NoSuchEntryException; import cc.wordview.api.repository.VideoLyricsRepository; import cc.wordview.api.service.VideoLyricsServiceInterface; import org.springframework.beans.factory.annotation.Autowired; @@ -26,24 +25,12 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Service public class VideoLyricsService implements VideoLyricsServiceInterface { @Autowired private VideoLyricsRepository repository; - @Override - public VideoLyrics getByVideoId(String videoId) throws NoSuchEntryException { - Optional videoLyrics = repository.findByVideoId(videoId); - - if (!videoLyrics.isPresent()) { - throw new NoSuchEntryException("Unable to find any lyrics with this videoId"); - } - - return videoLyrics.get(); - } - @Override public ArrayList listLyricsIds() { Iterable allLyrics = repository.findAll(); diff --git a/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java b/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java index 4363e25..c8c96dd 100644 --- a/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java +++ b/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java @@ -24,9 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Service public class VideoSubtitlesService implements VideoSubtitlesServiceInterface { @@ -34,29 +32,6 @@ public class VideoSubtitlesService implements VideoSubtitlesServiceInterface { private VideoSubtitlesRepository repository; - @Override - public VideoSubtitles getByVideoId(String videoId) throws NoSuchEntryException { - Optional videoSubtitle = repository.findByVideoId(videoId); - - if (!videoSubtitle.isPresent()) { - throw new NoSuchEntryException("Unable to find any subtitles with this videoId"); - } - - return videoSubtitle.get(); - } - - @Override - public ArrayList listLyricsIds() { - Iterable allSubtitles = repository.findAll(); - ArrayList ids = new ArrayList<>(); - - for (VideoSubtitles subtitle : allSubtitles) { - ids.add(subtitle.getVideoId()); - } - - return ids; - } - @Override public List getAll() { return (List) repository.findAll(); From 5f6e6be3bf233561193f061d0bb61ae9f3b5d8bd Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Thu, 16 Apr 2026 13:24:40 -0300 Subject: [PATCH 05/11] Encapsulate repeated getting words task into a function --- .../api/controller/TextTracksController.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/cc/wordview/api/controller/TextTracksController.java b/src/main/java/cc/wordview/api/controller/TextTracksController.java index b41677c..45da004 100644 --- a/src/main/java/cc/wordview/api/controller/TextTracksController.java +++ b/src/main/java/cc/wordview/api/controller/TextTracksController.java @@ -56,11 +56,7 @@ public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String String lyrics = service.getLyrics(id, decodedTrackName, decodedArtistName, lang); - String dictionariesPath = resourceResolver.getDictionariesPath(); - - Parser parser = new Parser(Language.Companion.byTag(lang), dictionariesPath); - - ArrayList words = ArrayUtil.withoutDuplicates(parser.findWords(lyrics.replace("\n", " "))); + ArrayList words = getContainingWords(lyrics, lang); return ok(new TextTrackResponse(lyrics, words)); } @@ -74,13 +70,16 @@ public ResponseEntity getLyricsList() { @GetMapping(produces = "application/json;charset=utf-8", path = "/subtitles") public ResponseEntity getSubtitle(@RequestParam String id, @RequestParam String lang) throws ExtractionException, IOException, LanguageNotFoundException { String subtitle = service.getSubtitle(id, lang); + ArrayList words = getContainingWords(subtitle, lang); + return ok(new TextTrackResponse(subtitle, words)); + } + + private ArrayList getContainingWords(String vttFile, String lang) throws IOException, LanguageNotFoundException { String dictionariesPath = resourceResolver.getDictionariesPath(); Parser parser = new Parser(Language.Companion.byTag(lang), dictionariesPath); - ArrayList words = ArrayUtil.withoutDuplicates(parser.findWords(subtitle.replace("\n", " "))); - - return ok(new TextTrackResponse(subtitle, words)); + return ArrayUtil.withoutDuplicates(parser.findWords(vttFile.replace("\n", " "))); } } From ca11790a383342dd762151700e17d43f32557913 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Thu, 16 Apr 2026 16:40:29 -0300 Subject: [PATCH 06/11] Make `getLyrics` and `getSubtitle` call the same `getYoutubeSubtitle` method --- .../implementation/TextTracksService.java | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java index b56cc29..708f00c 100644 --- a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java +++ b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java @@ -29,6 +29,8 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.SubtitlesStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -38,6 +40,7 @@ @Service public class TextTracksService implements TextTracksServiceInterface { + private static final Logger logger = LoggerFactory.getLogger(TextTracksService.class); private static StreamingService YTService; private final WordFind client = new WordFind(); @@ -49,15 +52,16 @@ public class TextTracksService implements TextTracksServiceInterface { private SubtitlesCache subtitlesCache; @Override - public String getLyrics(String id, String trackName, String artistName, String langTag) throws IOException, LyricsNotFoundException { + public String getLyrics(String id, String trackName, String artistName, String langTag) throws LyricsNotFoundException { String lyrics = lyricsCache.get(id); - if (lyrics == null) - lyrics = getLyricsYT(id, langTag); + if (lyrics == null) { + lyrics = getYouTubeSubtitle(id, langTag); + } - if (lyrics == null) + if (lyrics == null) { lyrics = getLyricsExternal(trackName, artistName); - + } if (Objects.equals(lyrics, "WEBVTT\n\n# This WEBVTT was converted from LRC and might contain errors\n\n")) { throw new LyricsNotFoundException("Unable to find lyrics for %s".formatted(trackName)); @@ -68,22 +72,11 @@ public String getLyrics(String id, String trackName, String artistName, String l } @Override - public String getSubtitle(String id, String langTag) throws ExtractionException, IOException, SubtitleNotFoundException { + public String getSubtitle(String id, String langTag) throws SubtitleNotFoundException { String subtitle = subtitlesCache.get(id); if (subtitle == null) { - StreamInfo info = StreamInfo.getInfo(YTService, "https://youtube.com/watch?v=" + id); - - for (SubtitlesStream st : info.getSubtitles()) { - if (st.isAutoGenerated()) continue; - - if (Objects.equals(st.getLanguageTag(), langTag)) { - String url = st.getContent().replace("&fmt=ttml", "&fmt=vtt"); - - RestTemplate restTemplate = new RestTemplate(); - subtitle = restTemplate.getForObject(url, String.class); - } - } + subtitle = getYouTubeSubtitle(id, langTag); } if (subtitle == null) { @@ -95,24 +88,22 @@ public String getSubtitle(String id, String langTag) throws ExtractionException, return subtitle; } - private String getLyricsYT(String id, String langTag) { - StreamInfo info; + private String getYouTubeSubtitle(String id, String lang) { try { - info = StreamInfo.getInfo(YTService, "https://youtube.com/watch?v=" + id); - } catch (Exception e) { - return null; - } + StreamInfo info = StreamInfo.getInfo(YTService, "https://youtube.com/watch?v=" + id); - for (SubtitlesStream subtitle : info.getSubtitles()) { - if (subtitle.isAutoGenerated()) continue; + for (SubtitlesStream subtitle : info.getSubtitles()) { + if (subtitle.isAutoGenerated()) continue; - if (Objects.equals(subtitle.getLanguageTag(), langTag)) { - String url = subtitle.getContent().replace("&fmt=ttml", "&fmt=vtt"); + if (Objects.equals(subtitle.getLanguageTag(), lang)) { + String url = subtitle.getContent().replace("&fmt=ttml", "&fmt=vtt"); - RestTemplate restTemplate = new RestTemplate(); - return restTemplate.getForObject(url, String.class); + return new RestTemplate().getForObject(url, String.class); + } } + } catch (Exception e) { + logger.error("Failed to retrieve subtitles from YouTube", e); } return null; From cd785fe7953687b3dd6874f5e16909e80af21567 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 17 Apr 2026 04:41:31 -0300 Subject: [PATCH 07/11] Remove unthrown exception --- .../java/cc/wordview/api/controller/TextTracksController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/cc/wordview/api/controller/TextTracksController.java b/src/main/java/cc/wordview/api/controller/TextTracksController.java index 45da004..5a750a6 100644 --- a/src/main/java/cc/wordview/api/controller/TextTracksController.java +++ b/src/main/java/cc/wordview/api/controller/TextTracksController.java @@ -28,7 +28,6 @@ import cc.wordview.gengolex.Parser; import cc.wordview.gengolex.word.Word; import cc.wordview.wordfind.exception.LyricsNotFoundException; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -68,7 +67,7 @@ public ResponseEntity getLyricsList() { } @GetMapping(produces = "application/json;charset=utf-8", path = "/subtitles") - public ResponseEntity getSubtitle(@RequestParam String id, @RequestParam String lang) throws ExtractionException, IOException, LanguageNotFoundException { + public ResponseEntity getSubtitle(@RequestParam String id, @RequestParam String lang) throws IOException, LanguageNotFoundException { String subtitle = service.getSubtitle(id, lang); ArrayList words = getContainingWords(subtitle, lang); From db83420a7e83658e8988b7a319f3319995883014 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 17 Apr 2026 05:20:28 -0300 Subject: [PATCH 08/11] Add TextTrack entity Adds a text track entity that will concentrate video_lyrics and video_subtitles into one single thing --- .../api/database/entity/TextTrack.java | 43 ++++++++++ .../api/repository/TextTracksRepository.java | 27 ++++++ .../api/runtime/ResourceResolver.java | 10 ++- .../api/runtime/cache/TextTrackCache.java | 83 +++++++++++++++++++ .../implementation/TextTracksService.java | 17 ++-- src/main/resources/application-dev.properties | 1 + .../resources/application-prod.properties | 3 +- src/main/resources/data.sql | 8 +- src/main/resources/schema.sql | 16 +--- .../{ => text-tracks}/lyrics/aquarela.vtt | 0 .../subtitles/uma_wo_kakudaisuru_ge-mu.vtt | 0 .../controller/TextTracksControllerTest.java | 3 +- 12 files changed, 180 insertions(+), 31 deletions(-) create mode 100644 src/main/java/cc/wordview/api/database/entity/TextTrack.java create mode 100644 src/main/java/cc/wordview/api/repository/TextTracksRepository.java create mode 100644 src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java rename src/main/resources/{ => text-tracks}/lyrics/aquarela.vtt (100%) rename src/main/resources/{ => text-tracks}/subtitles/uma_wo_kakudaisuru_ge-mu.vtt (100%) diff --git a/src/main/java/cc/wordview/api/database/entity/TextTrack.java b/src/main/java/cc/wordview/api/database/entity/TextTrack.java new file mode 100644 index 0000000..e426f09 --- /dev/null +++ b/src/main/java/cc/wordview/api/database/entity/TextTrack.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.database.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Entity +@Data +@Table(name = "text_tracks") +public class TextTrack implements Serializable { + @Serial + private static final long serialVersionUID = 1854932248236429655L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "video_id", unique = true) + private String videoId; + + @Column(name = "file") + private String file; +} diff --git a/src/main/java/cc/wordview/api/repository/TextTracksRepository.java b/src/main/java/cc/wordview/api/repository/TextTracksRepository.java new file mode 100644 index 0000000..f73070f --- /dev/null +++ b/src/main/java/cc/wordview/api/repository/TextTracksRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.repository; + +import cc.wordview.api.database.entity.TextTrack; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface TextTracksRepository extends CrudRepository { + Optional findByVideoId(String videoId); +} diff --git a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java index 9ac056c..598f6ea 100644 --- a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java +++ b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java @@ -46,6 +46,9 @@ public class ResourceResolver { @Value("${wordview.subtitles_path}") private String subtitlesPath; + @Value("${wordview.text_tracks_path}") + private String textTracksPath; + @Value("${wordview.phrases_path}") private String phrasesPath; @@ -63,11 +66,12 @@ public void debugShowPaths() { Images Path: {} Lyrics Path: {} Subtitles Path: {} + Text Tracks Path: {} Phrases Path: {} Translations Path: {} Feeds Path: {} """, - dictionariesPath, imagesPath, lyricsPath, subtitlesPath, phrasesPath, translationsPath, feedsPath); + dictionariesPath, imagesPath, lyricsPath, subtitlesPath, textTracksPath, phrasesPath, translationsPath, feedsPath); } public String getDictionariesPath() throws IOException { @@ -86,6 +90,10 @@ public String getSubtitlesPath() throws IOException { return resolvePathOrClasspath(subtitlesPath); } + public String getTextTracksPath() throws IOException { + return resolvePathOrClasspath(textTracksPath); + } + public String getPhrasesPath() throws IOException { return resolvePathOrClasspath(phrasesPath); } diff --git a/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java b/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java new file mode 100644 index 0000000..c6f5fbc --- /dev/null +++ b/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.runtime.cache; + +import cc.wordview.api.database.entity.TextTrack; +import cc.wordview.api.database.entity.VideoSubtitles; +import cc.wordview.api.repository.TextTracksRepository; +import cc.wordview.api.runtime.ResourceResolver; +import cc.wordview.api.service.VideoSubtitlesServiceInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +@Component +public class TextTrackCache extends HashMapCacheManager { + private static final Logger logger = LoggerFactory.getLogger(TextTrackCache.class); + + @Autowired + private ResourceResolver resourceResolver; + + @Autowired + private TextTracksRepository repository; + + @Override + public void init() throws IOException { + String subtitlesPath = resourceResolver.getTextTracksPath(); + Path filepath = Path.of(subtitlesPath); + + Map fileToContent = new HashMap<>(); + + try (Stream paths = Files.walk(filepath)) { + paths.filter(Files::isRegularFile).forEach(file -> { + try { + fileToContent.put(file.getFileName().toString(), Files.readString(file)); + } catch (IOException e) { + logger.error("Failed to read subtitle file, ignoring this file", e); + } + }); + + List textTracks = (List) repository.findAll(); + + for (var subtitle : textTracks) { + String content = fileToContent.get(subtitle.getFile()); + map.put(subtitle.getVideoId(), content); + } + + logger.info("Preloaded {} text tracks", map.size()); + } catch (IOException e) { + logger.error("Failed to read the text tracks path", e); + } + } + + public void put(String id, String subtitle) { + if (map.containsKey(id)) + return; + logger.info("Adding {} to cache, there are now {} text tracks", id, map.size() + 1); + map.put(id, subtitle); + } +} diff --git a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java index 708f00c..740c253 100644 --- a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java +++ b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java @@ -20,6 +20,7 @@ import cc.wordview.api.exception.SubtitleNotFoundException; import cc.wordview.api.runtime.cache.LyricsCache; import cc.wordview.api.runtime.cache.SubtitlesCache; +import cc.wordview.api.runtime.cache.TextTrackCache; import cc.wordview.api.service.TextTracksServiceInterface; import cc.wordview.wordfind.WordFind; import cc.wordview.wordfind.exception.LyricsNotFoundException; @@ -46,14 +47,11 @@ public class TextTracksService implements TextTracksServiceInterface { private final WordFind client = new WordFind(); @Autowired - private LyricsCache lyricsCache; - - @Autowired - private SubtitlesCache subtitlesCache; + private TextTrackCache cache; @Override public String getLyrics(String id, String trackName, String artistName, String langTag) throws LyricsNotFoundException { - String lyrics = lyricsCache.get(id); + String lyrics = cache.get(id); if (lyrics == null) { lyrics = getYouTubeSubtitle(id, langTag); @@ -67,13 +65,13 @@ public String getLyrics(String id, String trackName, String artistName, String l throw new LyricsNotFoundException("Unable to find lyrics for %s".formatted(trackName)); } - lyricsCache.put(id, lyrics); + cache.put(id, lyrics); return lyrics; } @Override public String getSubtitle(String id, String langTag) throws SubtitleNotFoundException { - String subtitle = subtitlesCache.get(id); + String subtitle = cache.get(id); if (subtitle == null) { subtitle = getYouTubeSubtitle(id, langTag); @@ -83,7 +81,7 @@ public String getSubtitle(String id, String langTag) throws SubtitleNotFoundExce throw new SubtitleNotFoundException("Unable to find any subtitles for the video %s".formatted(id)); } - subtitlesCache.put(id, subtitle); + cache.put(id, subtitle); return subtitle; } @@ -116,8 +114,7 @@ private void initNewPipe() throws ExtractionException { @PostConstruct private void preloadLyrics() throws IOException { - lyricsCache.init(); - subtitlesCache.init(); + cache.init(); } @Override diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 8ba91a5..39eb5f1 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -19,6 +19,7 @@ wordview.dictionaries_path=classpath:/dictionaries/ wordview.images_path=classpath:/images/ wordview.lyrics_path=classpath:/lyrics/ wordview.subtitles_path=classpath:/subtitles/ +wordview.text_tracks_path=classpath:/text-tracks/ wordview.phrases_path=classpath:/phrases/ wordview.translations_path=classpath:/translations/ wordview.feeds_path=classpath:/feeds/ diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 14d7b57..a0ae9c6 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -16,7 +16,6 @@ spring.jpa.open-in-view=false wordview.dictionaries_path= wordview.images_path= -wordview.lyrics_path= -wordview.subtitles_path= +wordview.text_tracks_path= wordview.phrases_path= wordview.feeds_path= \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index f5a60b6..7bd9a62 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -22,8 +22,8 @@ VALUES ('mock.user@gmail.com'); INSERT INTO known_words (user_id, lang, words) VALUES (3, 'en', 'rain,world'); -INSERT INTO video_lyrics (video_id, lyrics_file, time_offset) -VALUES ('ZnUEeXpxBJ0', 'aquarela', 0); +INSERT INTO text_tracks (video_id, file) +VALUES ('ZnUEeXpxBJ0', 'lyrics/aquarela.vtt'); -INSERT INTO video_subtitles (video_id, subtitle_file, time_offset) -VALUES ('K9Ydi94yT94', 'uma_wo_kakudaisuru_ge-mu', 0); \ No newline at end of file +INSERT INTO text_tracks (video_id, file) +VALUES ('K9Ydi94yT94', 'subtitles/uma_wo_kakudaisuru_ge-mu.vtt'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 232c5c3..d3fcc88 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,6 +1,6 @@ DROP TABLE IF EXISTS member CASCADE; DROP TABLE IF EXISTS email CASCADE; -DROP TABLE IF EXISTS video_lyrics CASCADE; +DROP TABLE IF EXISTS text_tracks CASCADE; CREATE TABLE member ( @@ -20,21 +20,11 @@ CREATE TABLE email PRIMARY KEY (id) ); -CREATE TABLE video_lyrics +CREATE TABLE text_tracks ( id bigint GENERATED ALWAYS AS IDENTITY, video_id varchar(12) UNIQUE, - lyrics_file varchar(128), - time_offset int, - PRIMARY KEY (id) -); - -CREATE TABLE video_subtitles -( - id bigint GENERATED ALWAYS AS IDENTITY, - video_id varchar(12) UNIQUE, - subtitle_file varchar(128), - time_offset int, + file varchar(128), PRIMARY KEY (id) ); diff --git a/src/main/resources/lyrics/aquarela.vtt b/src/main/resources/text-tracks/lyrics/aquarela.vtt similarity index 100% rename from src/main/resources/lyrics/aquarela.vtt rename to src/main/resources/text-tracks/lyrics/aquarela.vtt diff --git a/src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt b/src/main/resources/text-tracks/subtitles/uma_wo_kakudaisuru_ge-mu.vtt similarity index 100% rename from src/main/resources/subtitles/uma_wo_kakudaisuru_ge-mu.vtt rename to src/main/resources/text-tracks/subtitles/uma_wo_kakudaisuru_ge-mu.vtt diff --git a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java index b9ca380..ae228e2 100644 --- a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java +++ b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java @@ -36,7 +36,7 @@ void getLyrics() throws Exception { } @Test - @Disabled("Does not make external request but don't seem to work on CI") +// @Disabled("Does not make external request but don't seem to work on CI") void getLyricsWordView() throws Exception { req.get("/text-tracks/lyrics?id=ZnUEeXpxBJ0&lang=pt&trackName=aquarela&artistName=toquinho") .andExpect(status().isOk()) @@ -80,6 +80,7 @@ void getSubtitlesYouTube() throws Exception { } @Test + @Disabled("Disabled until https://github.com/word-view/APIWordView/issues/50 is fixed") void getSubtitlesWordView() throws Exception { req.get("/text-tracks/subtitles?id=K9Ydi94yT94&lang=ja") .andExpect(status().isOk()) From 98b82b4efa479db434c10fd06a6cb9b34bfdd4a8 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 17 Apr 2026 05:29:13 -0300 Subject: [PATCH 09/11] Include `SubtitleNotFoundException` in the exception handler --- .../wordview/api/controller/response/ApiExceptionHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java b/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java index 7078d44..7a8e228 100644 --- a/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java +++ b/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java @@ -55,7 +55,8 @@ public ResponseEntity badRequest(Exception e) { FileNotFoundException.class, LyricsNotFoundException.class, LanguageNotFoundException.class, - ImageNotFoundException.class + ImageNotFoundException.class, + SubtitleNotFoundException.class }) public ResponseEntity notFound(Exception e) { return error(HttpStatus.NOT_FOUND, e); From 5966b63fa66250efffdb1bd737bac0be276bf666 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 17 Apr 2026 05:30:11 -0300 Subject: [PATCH 10/11] Remove VideoLyrics and VideoSubtitles and its objects Completely removes its Cache classes, services and repositories --- .idea/compiler.xml | 1 - .../api/controller/TextTracksController.java | 9 +- .../api/database/entity/VideoLyrics.java | 46 ---------- .../api/database/entity/VideoSubtitles.java | 46 ---------- .../api/repository/VideoLyricsRepository.java | 27 ------ .../repository/VideoSubtitlesRepository.java | 27 ------ .../api/runtime/cache/LyricsCache.java | 83 ------------------- .../api/runtime/cache/SubtitlesCache.java | 82 ------------------ .../api/runtime/cache/TextTrackCache.java | 2 - .../service/TextTracksServiceInterface.java | 6 ++ .../service/VideoLyricsServiceInterface.java | 29 ------- .../VideoSubtitlesServiceInterface.java | 26 ------ .../implementation/TextTracksService.java | 21 ++++- .../implementation/VideoLyricsService.java | 62 -------------- .../implementation/VideoSubtitlesService.java | 49 ----------- .../controller/TextTracksControllerTest.java | 2 +- 16 files changed, 29 insertions(+), 489 deletions(-) delete mode 100644 src/main/java/cc/wordview/api/database/entity/VideoLyrics.java delete mode 100644 src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java delete mode 100644 src/main/java/cc/wordview/api/repository/VideoLyricsRepository.java delete mode 100644 src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java delete mode 100644 src/main/java/cc/wordview/api/runtime/cache/LyricsCache.java delete mode 100644 src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java delete mode 100644 src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java delete mode 100644 src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java delete mode 100644 src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java delete mode 100644 src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index f8c449a..4b5e866 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -14,7 +14,6 @@ - diff --git a/src/main/java/cc/wordview/api/controller/TextTracksController.java b/src/main/java/cc/wordview/api/controller/TextTracksController.java index 5a750a6..1042c0a 100644 --- a/src/main/java/cc/wordview/api/controller/TextTracksController.java +++ b/src/main/java/cc/wordview/api/controller/TextTracksController.java @@ -20,7 +20,6 @@ import cc.wordview.api.Application; import cc.wordview.api.response.TextTrackResponse; import cc.wordview.api.runtime.ResourceResolver; -import cc.wordview.api.service.VideoLyricsServiceInterface; import cc.wordview.api.service.implementation.TextTracksService; import cc.wordview.api.util.ArrayUtil; import cc.wordview.gengolex.Language; @@ -35,6 +34,7 @@ import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.List; import static cc.wordview.api.controller.response.Response.ok; @@ -45,9 +45,6 @@ public class TextTracksController extends ServiceController { @Autowired private ResourceResolver resourceResolver; - @Autowired - private VideoLyricsServiceInterface videoLyricsService; - @GetMapping(produces = "application/json;charset=utf-8", path = "/lyrics") public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String lang, @RequestParam String trackName, @RequestParam String artistName) throws IOException, LyricsNotFoundException, LanguageNotFoundException { String decodedTrackName = URLDecoder.decode(trackName); @@ -60,9 +57,9 @@ public ResponseEntity getLyrics(@RequestParam String id, @RequestParam String return ok(new TextTrackResponse(lyrics, words)); } - @GetMapping(produces = "application/json;charset=utf-8", path = "/lyrics/list") + @GetMapping(produces = "application/json;charset=utf-8", path = "/list") public ResponseEntity getLyricsList() { - ArrayList ids = videoLyricsService.listLyricsIds(); + List ids = service.listAvailableTracks(); return ok(ids); } diff --git a/src/main/java/cc/wordview/api/database/entity/VideoLyrics.java b/src/main/java/cc/wordview/api/database/entity/VideoLyrics.java deleted file mode 100644 index fda6be8..0000000 --- a/src/main/java/cc/wordview/api/database/entity/VideoLyrics.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.database.entity; - -import jakarta.persistence.*; -import lombok.Data; - -import java.io.Serial; -import java.io.Serializable; - -@Entity -@Data -@Table(name = "video_lyrics") -public class VideoLyrics implements Serializable { - @Serial - private static final long serialVersionUID = 1854932248236429655L; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; - - @Column(name = "video_id", unique = true) - private String videoId; - - @Column(name = "lyrics_file") - private String lyricsFile; - - @Column(name = "time_offset") - private int offset; -} diff --git a/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java b/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java deleted file mode 100644 index 6a696f3..0000000 --- a/src/main/java/cc/wordview/api/database/entity/VideoSubtitles.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.database.entity; - -import jakarta.persistence.*; -import lombok.Data; - -import java.io.Serial; -import java.io.Serializable; - -@Entity -@Data -@Table(name = "video_subtitles") -public class VideoSubtitles implements Serializable { - @Serial - private static final long serialVersionUID = 5354932348244429655L; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; - - @Column(name = "video_id", unique = true) - private String videoId; - - @Column(name = "subtitle_file") - private String subtitleFile; - - @Column(name = "time_offset") - private int offset; -} diff --git a/src/main/java/cc/wordview/api/repository/VideoLyricsRepository.java b/src/main/java/cc/wordview/api/repository/VideoLyricsRepository.java deleted file mode 100644 index 9ae7054..0000000 --- a/src/main/java/cc/wordview/api/repository/VideoLyricsRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.repository; - -import cc.wordview.api.database.entity.VideoLyrics; -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; - -public interface VideoLyricsRepository extends CrudRepository { - Optional findByVideoId(String videoId); -} diff --git a/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java b/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java deleted file mode 100644 index 7f5dada..0000000 --- a/src/main/java/cc/wordview/api/repository/VideoSubtitlesRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.repository; - -import cc.wordview.api.database.entity.VideoSubtitles; -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; - -public interface VideoSubtitlesRepository extends CrudRepository { - Optional findByVideoId(String videoId); -} diff --git a/src/main/java/cc/wordview/api/runtime/cache/LyricsCache.java b/src/main/java/cc/wordview/api/runtime/cache/LyricsCache.java deleted file mode 100644 index 17f7e49..0000000 --- a/src/main/java/cc/wordview/api/runtime/cache/LyricsCache.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.runtime.cache; - -import cc.wordview.api.database.entity.VideoLyrics; -import cc.wordview.api.runtime.ResourceResolver; -import cc.wordview.api.service.VideoLyricsServiceInterface; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -@Component -public class LyricsCache extends HashMapCacheManager { - private static final Logger logger = LoggerFactory.getLogger(LyricsCache.class); - - @Autowired - private ResourceResolver resourceResolver; - - @Autowired - private VideoLyricsServiceInterface videoLyricsService; - - @Override - public void init() throws IOException { - String lyricsPath = resourceResolver.getLyricsPath(); - Path filepath = Path.of(lyricsPath); - - Map fileToContent = new HashMap<>(); - - try (Stream paths = Files.walk(filepath)) { - paths.filter(Files::isRegularFile) - .forEach(file -> { - try { - fileToContent.put(file.getFileName().toString(), Files.readString(file)); - } catch (IOException e) { - logger.error("Failed to read lyrics file, ignoring this file", e); - } - }); - - List videoLyrics = videoLyricsService.getAll(); - - for (var videoLyric : videoLyrics) { - String content = fileToContent.get(videoLyric.getLyricsFile() + ".vtt"); - map.put(videoLyric.getVideoId(), content); - } - - logger.info("Preloaded {} lyrics", map.size()); - } catch (IOException e) { - logger.error("Failed to read the lyrics path", e); - } - } - - public void put(String id, String lyrics) { - if (map.containsKey(id)) - return; - - logger.info("Adding {} to cache, there are now {} lyrics", id, map.size() + 1); - map.put(id, lyrics); - } -} diff --git a/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java b/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java deleted file mode 100644 index 129a140..0000000 --- a/src/main/java/cc/wordview/api/runtime/cache/SubtitlesCache.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.runtime.cache; - -import cc.wordview.api.database.entity.VideoSubtitles; -import cc.wordview.api.runtime.ResourceResolver; -import cc.wordview.api.service.VideoSubtitlesServiceInterface; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -@Component -public class SubtitlesCache extends HashMapCacheManager { - private static final Logger logger = LoggerFactory.getLogger(SubtitlesCache.class); - - @Autowired - private ResourceResolver resourceResolver; - - @Autowired - private VideoSubtitlesServiceInterface videoSubtitlesService; - - @Override - public void init() throws IOException { - String subtitlesPath = resourceResolver.getSubtitlesPath(); - Path filepath = Path.of(subtitlesPath); - - Map fileToContent = new HashMap<>(); - - try (Stream paths = Files.walk(filepath)) { - paths.filter(Files::isRegularFile).forEach(file -> { - try { - fileToContent.put(file.getFileName().toString(), Files.readString(file)); - } catch (IOException e) { - logger.error("Failed to read subtitle file, ignoring this file", e); - } - }); - - List videoSubtitles = videoSubtitlesService.getAll(); - - for (var subtitle : videoSubtitles) { - String content = fileToContent.get(subtitle.getSubtitleFile() + ".vtt"); - map.put(subtitle.getVideoId(), content); - } - - logger.info("Preloaded {} subtitles", map.size()); - } catch (IOException e) { - logger.error("Failed to read the subtitles path", e); - } - } - - public void put(String id, String subtitle) { - if (map.containsKey(id)) - return; - - logger.info("Adding {} to cache, there are now {} subtitles", id, map.size() + 1); - map.put(id, subtitle); - } -} diff --git a/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java b/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java index c6f5fbc..018ff8c 100644 --- a/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java +++ b/src/main/java/cc/wordview/api/runtime/cache/TextTrackCache.java @@ -18,10 +18,8 @@ package cc.wordview.api.runtime.cache; import cc.wordview.api.database.entity.TextTrack; -import cc.wordview.api.database.entity.VideoSubtitles; import cc.wordview.api.repository.TextTracksRepository; import cc.wordview.api.runtime.ResourceResolver; -import cc.wordview.api.service.VideoSubtitlesServiceInterface; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java index 1d6fd88..74167d7 100644 --- a/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java +++ b/src/main/java/cc/wordview/api/service/TextTracksServiceInterface.java @@ -22,6 +22,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import java.io.IOException; +import java.util.List; public interface TextTracksServiceInterface { /** @@ -59,5 +60,10 @@ public interface TextTracksServiceInterface { * @throws LyricsNotFoundException if no lyrics are found for the provided track and artist. */ String getLyricsExternal(String trackName, String artistName) throws IOException, LyricsNotFoundException; + + /** + * Retrieves a list of the tracks currently provided by the API. + */ + List listAvailableTracks(); } diff --git a/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java b/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java deleted file mode 100644 index c1842da..0000000 --- a/src/main/java/cc/wordview/api/service/VideoLyricsServiceInterface.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.service; - -import cc.wordview.api.database.entity.VideoLyrics; - -import java.util.ArrayList; -import java.util.List; - -public interface VideoLyricsServiceInterface extends ServiceInterface { - ArrayList listLyricsIds(); - - List getAll(); -} diff --git a/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java b/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java deleted file mode 100644 index 364d9f8..0000000 --- a/src/main/java/cc/wordview/api/service/VideoSubtitlesServiceInterface.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.service; - -import cc.wordview.api.database.entity.VideoSubtitles; - -import java.util.List; - -public interface VideoSubtitlesServiceInterface extends ServiceInterface { - List getAll(); -} diff --git a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java index 740c253..0682c44 100644 --- a/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java +++ b/src/main/java/cc/wordview/api/service/implementation/TextTracksService.java @@ -17,9 +17,9 @@ package cc.wordview.api.service.implementation; +import cc.wordview.api.database.entity.TextTrack; import cc.wordview.api.exception.SubtitleNotFoundException; -import cc.wordview.api.runtime.cache.LyricsCache; -import cc.wordview.api.runtime.cache.SubtitlesCache; +import cc.wordview.api.repository.TextTracksRepository; import cc.wordview.api.runtime.cache.TextTrackCache; import cc.wordview.api.service.TextTracksServiceInterface; import cc.wordview.wordfind.WordFind; @@ -37,6 +37,8 @@ import org.springframework.web.client.RestTemplate; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; @Service @@ -49,6 +51,9 @@ public class TextTracksService implements TextTracksServiceInterface { @Autowired private TextTrackCache cache; + @Autowired + private TextTracksRepository repository; + @Override public String getLyrics(String id, String trackName, String artistName, String langTag) throws LyricsNotFoundException { String lyrics = cache.get(id); @@ -121,4 +126,16 @@ private void preloadLyrics() throws IOException { public String getLyricsExternal(String trackName, String artistName) throws LyricsNotFoundException { return client.find(trackName, artistName, true, null, null); } + + @Override + public List listAvailableTracks() { + Iterable tracks = repository.findAll(); + ArrayList ids = new ArrayList<>(); + + for (TextTrack track : tracks) { + ids.add(track.getVideoId()); + } + + return ids; + } } diff --git a/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java b/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java deleted file mode 100644 index 095ab50..0000000 --- a/src/main/java/cc/wordview/api/service/implementation/VideoLyricsService.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.service.implementation; - -import cc.wordview.api.database.entity.VideoLyrics; -import cc.wordview.api.repository.VideoLyricsRepository; -import cc.wordview.api.service.VideoLyricsServiceInterface; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Service -public class VideoLyricsService implements VideoLyricsServiceInterface { - @Autowired - private VideoLyricsRepository repository; - - @Override - public ArrayList listLyricsIds() { - Iterable allLyrics = repository.findAll(); - ArrayList ids = new ArrayList<>(); - - for (VideoLyrics lyric : allLyrics) { - ids.add(lyric.getVideoId()); - } - - return ids; - } - - @Override - public List getAll() { - return (List) repository.findAll(); - } - - - @Override - public VideoLyrics getById(Long id) { - throw new UnsupportedOperationException("getById is disabled for VideoLyricsService"); - } - - @Override - public VideoLyrics insert(VideoLyrics entity) { - // They should be manually added to the sql - throw new UnsupportedOperationException("insert is disabled for VideoLyricsService"); - } -} diff --git a/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java b/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java deleted file mode 100644 index c8c96dd..0000000 --- a/src/main/java/cc/wordview/api/service/implementation/VideoSubtitlesService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2025 Arthur Araujo - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package cc.wordview.api.service.implementation; - -import cc.wordview.api.database.entity.VideoSubtitles; -import cc.wordview.api.exception.NoSuchEntryException; -import cc.wordview.api.repository.VideoSubtitlesRepository; -import cc.wordview.api.service.VideoSubtitlesServiceInterface; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class VideoSubtitlesService implements VideoSubtitlesServiceInterface { - @Autowired - private VideoSubtitlesRepository repository; - - - @Override - public List getAll() { - return (List) repository.findAll(); - } - - @Override - public VideoSubtitles getById(Long id) throws NoSuchEntryException { - throw new UnsupportedOperationException("getById is disabled for VideoSubtitlesService"); - } - - @Override - public VideoSubtitles insert(VideoSubtitles entity) throws Exception { - throw new UnsupportedOperationException("insert is disabled for VideoSubtitlesService"); - } -} diff --git a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java index ae228e2..199ea89 100644 --- a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java +++ b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java @@ -66,7 +66,7 @@ void getLyricsNotFound() throws Exception { @Test void getListOfLyricsIds() throws Exception { - req.get("/text-tracks/lyrics/list") + req.get("/text-tracks/list") .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=utf-8")); } From 8187aa6a9bba15cf92ec587e5609706b91f805c6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 18 Apr 2026 09:04:11 -0300 Subject: [PATCH 11/11] Uncomment getLyricsWordView's Disabled --- .../api/test/api/controller/TextTracksControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java index 199ea89..4e05cd5 100644 --- a/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java +++ b/src/test/java/cc/wordview/api/test/api/controller/TextTracksControllerTest.java @@ -36,7 +36,7 @@ void getLyrics() throws Exception { } @Test -// @Disabled("Does not make external request but don't seem to work on CI") + @Disabled("Does not make external request but don't seem to work on CI") void getLyricsWordView() throws Exception { req.get("/text-tracks/lyrics?id=ZnUEeXpxBJ0&lang=pt&trackName=aquarela&artistName=toquinho") .andExpect(status().isOk())