diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1aa6bba..ec40111 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -11,6 +11,15 @@ + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9ab92fa..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/app/dto/api-types.ts b/frontend/src/app/dto/api-types.ts index 80d34ca..6183a1f 100644 --- a/frontend/src/app/dto/api-types.ts +++ b/frontend/src/app/dto/api-types.ts @@ -1,6 +1,6 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 4.1.1 on 2026-07-24 16:46:55. +// Generated using typescript-generator version 4.1.1 on 2026-07-26 14:02:29. export interface Session { sessionId: string; diff --git a/frontend/src/app/youtube-search-result/youtube-search-result.ts b/frontend/src/app/youtube-search-result/youtube-search-result.ts index 910cccc..a3c946c 100644 --- a/frontend/src/app/youtube-search-result/youtube-search-result.ts +++ b/frontend/src/app/youtube-search-result/youtube-search-result.ts @@ -35,7 +35,7 @@ export class YoutubeSearchResult implements OnChanges { this._album = this.musicRecord.album?.name || 'Album Name'; this._duration = this.musicRecord.duration || '0:00'; this._views = this.musicRecord.views || '0 views'; - this._thumbnailUrl = this.musicRecord.thumbnails?.[1]?.url || this._thumbnailUrl; + this._thumbnailUrl = this.musicRecord.thumbnails?.[0]?.url || this._thumbnailUrl; } } diff --git a/frontend/src/app/youtube-search-tab/youtube-search-tab.html b/frontend/src/app/youtube-search-tab/youtube-search-tab.html index 00730ea..6737bec 100644 --- a/frontend/src/app/youtube-search-tab/youtube-search-tab.html +++ b/frontend/src/app/youtube-search-tab/youtube-search-tab.html @@ -3,6 +3,14 @@ Search + + + Filter + + {{f}} + + + diff --git a/frontend/src/app/youtube-search-tab/youtube-search-tab.ts b/frontend/src/app/youtube-search-tab/youtube-search-tab.ts index 893367e..a084971 100644 --- a/frontend/src/app/youtube-search-tab/youtube-search-tab.ts +++ b/frontend/src/app/youtube-search-tab/youtube-search-tab.ts @@ -11,23 +11,47 @@ import { YoutubeSearchResult } from '../youtube-search-result/youtube-search-res import { CommonModule } from '@angular/common'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-youtube-search-tab', - imports: [YoutubeSearchResult, CommonModule, MatFormFieldModule, MatInputModule, MatButtonModule, FormsModule], + imports: [ + YoutubeSearchResult, + CommonModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatButtonModule, + FormsModule, + ], templateUrl: './youtube-search-tab.html', - styleUrl: './youtube-search-tab.css' + styleUrl: './youtube-search-tab.css', }) export class YoutubeSearchTab { private readonly youtubeService = inject(YoutubeControllerService); searchResults: WritableSignal = signal([]); searchQuery: string = ''; + searchFilter: QueryDto['filter'] = 'SONGS'; + filters: QueryDto['filter'][] = [ + 'SONGS', + 'VIDEOS', + 'ALBUMS', + 'ARTISTS', + 'PLAYLISTS', + 'COMMUNITY_PLAYLISTS', + 'FEATURED_PLALISTS', + 'PROFILES', + 'PODCASTS', + 'EPISODES', + 'NONE', + ]; performSearch(): void { const queryDto: QueryDto = { queryString: this.searchQuery, + filter: this.searchFilter, }; this.youtubeService.query(queryDto).subscribe({ @@ -40,8 +64,5 @@ export class YoutubeSearchTab { console.error('Query error:', error); }, }); - - - } } diff --git a/musix/pom.xml b/musix/pom.xml index 13ad916..cd4b0fd 100644 --- a/musix/pom.xml +++ b/musix/pom.xml @@ -88,6 +88,11 @@ springdoc-openapi-starter-webmvc-ui 2.8.5 + + org.apache.commons + commons-text + 1.10.0 + @@ -168,12 +173,12 @@ - + diff --git a/musix/python/query.py b/musix/python/query.py index 703dab6..74e4337 100755 --- a/musix/python/query.py +++ b/musix/python/query.py @@ -16,7 +16,13 @@ if(args.limit != None): limit=args.limit; if(args.filter != None): filter=args.filter - +tries = 1 ytmusic = YTMusic() search_results = ytmusic.search(args.query, filter=filter, limit = limit) +while(len(search_results)< limit and tries <10): + limit = limit*2 + search_results = ytmusic.search(args.query, filter=filter, limit = limit) + tries = tries + 1 + + print(search_results) diff --git a/musix/src/main/java/zv/musix/controller/ClientController.java b/musix/src/main/java/zv/musix/controller/ClientController.java index 020e8e0..0268c81 100644 --- a/musix/src/main/java/zv/musix/controller/ClientController.java +++ b/musix/src/main/java/zv/musix/controller/ClientController.java @@ -1,6 +1,8 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Description; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -18,6 +20,12 @@ public class ClientController { @Autowired private ClientService clientService; + @Operation( + summary = "Client registration", + description = "End point for MPV clients to register themselves to the server. " + + "This is used to send MPV commands (like 'play') to each MPV client" + + "The request must contain the host url where the server can reach the MPV client" + ) @PostMapping("/register") public ResponseEntity registerClient(@RequestBody ClientRegistrationRequestDto clientRegistrationRequestDto){ return ResponseEntity.ok(clientService.register(clientRegistrationRequestDto)); diff --git a/musix/src/main/java/zv/musix/controller/HealthController.java b/musix/src/main/java/zv/musix/controller/HealthController.java index 58e08cb..50be3bd 100644 --- a/musix/src/main/java/zv/musix/controller/HealthController.java +++ b/musix/src/main/java/zv/musix/controller/HealthController.java @@ -1,5 +1,6 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,6 +10,10 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE) public class HealthController { + @Operation( + summary = "Ping Pong", + description = "End point used to check if the server is up and running (mainly for docker compose health check)" + ) @GetMapping("/ping") public String ping(){ return "pong"; diff --git a/musix/src/main/java/zv/musix/controller/PlayerController.java b/musix/src/main/java/zv/musix/controller/PlayerController.java index b723dd8..a97de89 100644 --- a/musix/src/main/java/zv/musix/controller/PlayerController.java +++ b/musix/src/main/java/zv/musix/controller/PlayerController.java @@ -1,5 +1,6 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -17,6 +18,10 @@ public class PlayerController { @Autowired private PlayerService playerService; + @Operation( + summary = "Play song with id", + description = "Searches the music from the cache and if it founds it, sends a request to each MPV client to play the requested music" + ) @PostMapping("/play/{id}") public ResponseEntity play(@PathVariable String id){ playerService.play(id); diff --git a/musix/src/main/java/zv/musix/controller/SessionController.java b/musix/src/main/java/zv/musix/controller/SessionController.java index 95e7b32..19880b3 100644 --- a/musix/src/main/java/zv/musix/controller/SessionController.java +++ b/musix/src/main/java/zv/musix/controller/SessionController.java @@ -1,5 +1,6 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -16,11 +17,21 @@ public class SessionController { private SessionService sessionService; + @Operation( + summary = "Session information", + description = "Returns the current state of the session, includes User data, their queues, " + + "The session queue etc etc" + ) @GetMapping public ResponseEntity getSession(){ return ResponseEntity.ok(sessionService.getSession()); } + @Operation( + summary = "Add music to user's queue", + description = "Requires music ID and userName on the Authorization Request Header" + + ) @PostMapping("/music/add/{musicId}") public ResponseEntity addMusic(@RequestHeader("Authorization") String userName, @PathVariable String musicId){ return ResponseEntity.ok(sessionService.addMusicToQueue(userName,musicId)); diff --git a/musix/src/main/java/zv/musix/controller/UserController.java b/musix/src/main/java/zv/musix/controller/UserController.java index b7de70e..6f246d3 100644 --- a/musix/src/main/java/zv/musix/controller/UserController.java +++ b/musix/src/main/java/zv/musix/controller/UserController.java @@ -1,5 +1,6 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -17,6 +18,12 @@ public class UserController { @Autowired SessionService sessionService; + + @Operation( + summary = "Add new User to session", + description = "Creates a new user in the session, and returns the user list from the session" + + ) @PostMapping public ResponseEntity> createUser(@RequestBody CreateUserRequest userRequest) { return ResponseEntity.ok(sessionService.addUser(userRequest)); diff --git a/musix/src/main/java/zv/musix/controller/YoutubeController.java b/musix/src/main/java/zv/musix/controller/YoutubeController.java index 65ca252..dceec31 100644 --- a/musix/src/main/java/zv/musix/controller/YoutubeController.java +++ b/musix/src/main/java/zv/musix/controller/YoutubeController.java @@ -1,5 +1,6 @@ package zv.musix.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -8,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import zv.musix.dto.yt.QueryDto; -import zv.musix.entity.QueryCache; import zv.musix.python.json.MusicRecord; import zv.musix.service.YtService; @@ -22,6 +22,14 @@ public class YoutubeController { private YtService ytService; + @Operation( + summary = "Youtube search", + description = "Queries youtube based on the given parameters" + + "queryString = The string you are searching for" + + "filter = the type of result you are looking for, for example songs are things youtube.music would display as a result, videos are default youtube, playlists are playlists etc." + + "limit = the number of results to return " + + ) @PostMapping("/query") public ResponseEntity> query(@RequestBody QueryDto queryDto ) { return ResponseEntity.ok(ytService.query(queryDto)); diff --git a/musix/src/main/java/zv/musix/dto/yt/QueryDto.java b/musix/src/main/java/zv/musix/dto/yt/QueryDto.java index 1e854d9..ab32f9f 100644 --- a/musix/src/main/java/zv/musix/dto/yt/QueryDto.java +++ b/musix/src/main/java/zv/musix/dto/yt/QueryDto.java @@ -6,7 +6,6 @@ import zv.musix.enums.YTQueryFilter; @Data public class QueryDto { String queryString; - YTQueryFilter filter; Integer limit; } diff --git a/musix/src/main/java/zv/musix/entity/QueryCache.java b/musix/src/main/java/zv/musix/entity/QueryCache.java index bdcaf73..c5456c0 100644 --- a/musix/src/main/java/zv/musix/entity/QueryCache.java +++ b/musix/src/main/java/zv/musix/entity/QueryCache.java @@ -23,10 +23,10 @@ public class QueryCache { @TimeToLive private Long timeoutInSeconds = 600L; - public QueryCache(QueryDto dto){ - this.query = dto.getQueryString(); - this.filter = dto.getFilter() == null ? YTQueryFilter.SONGS : dto.getFilter(); - this.limit = dto.getLimit() == null ? 20 : dto.getLimit() ; + public QueryCache(String query, YTQueryFilter filter, Integer limit ){ + this.query = query; + this.filter = filter; + this.limit = limit == null ? 20 : limit ; this.id = query.hashCode() + filter.label + limit; } diff --git a/musix/src/main/java/zv/musix/python/PythonRunner.java b/musix/src/main/java/zv/musix/python/PythonRunner.java index 1f723c2..78cacdd 100644 --- a/musix/src/main/java/zv/musix/python/PythonRunner.java +++ b/musix/src/main/java/zv/musix/python/PythonRunner.java @@ -45,19 +45,19 @@ public class PythonRunner { } - public String query_py(QueryDto request){ + public String query_py(String query, YTQueryFilter filter, Integer limit){ ArrayList args = new ArrayList<>(); args.add(PYTHON); args.add(QUERY_PY); args.add("--query"); - args.add(request.getQueryString()); - if(request.getFilter() != null && request.getFilter() != YTQueryFilter.NONE){ + args.add(query); + if(filter != null && filter != YTQueryFilter.NONE){ args.add("--filter"); - args.add(request.getFilter().label); + args.add(filter.label); } - if(request.getLimit() != null) { + if(limit != null) { args.add("--limit"); - args.add(request.getLimit().toString()); + args.add(limit.toString()); } return runScript(args); } diff --git a/musix/src/main/java/zv/musix/service/YtService.java b/musix/src/main/java/zv/musix/service/YtService.java index e73ac6e..6cbe8a6 100644 --- a/musix/src/main/java/zv/musix/service/YtService.java +++ b/musix/src/main/java/zv/musix/service/YtService.java @@ -2,10 +2,12 @@ package zv.musix.service; import com.google.gson.Gson; import lombok.extern.log4j.Log4j2; +import org.apache.commons.text.similarity.JaroWinklerDistance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import zv.musix.dto.yt.QueryDto; import zv.musix.entity.QueryCache; +import zv.musix.enums.YTQueryFilter; import zv.musix.python.PythonRunner; import zv.musix.python.json.MusicRecord; import zv.musix.repository.MusicRepository; @@ -14,8 +16,11 @@ import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @Log4j2 @@ -30,34 +35,66 @@ public class YtService { public List query(QueryDto request){ + JaroWinklerDistance jaroWinkler = new JaroWinklerDistance(); + log.trace("Search query in cache: {}", request); - QueryCache qc = new QueryCache(request); + List queryCacheList = new ArrayList<>(); + List result = new ArrayList<>(); + + queryCacheList.add(performSearch(request.getQueryString(),YTQueryFilter.SONGS, request.getLimit())); + //queryCacheList.add(performSearch(request.getQueryString(),YTQueryFilter.VIDEOS, request.getLimit())); + //queryCacheList.add(performSearch(request.getQueryString(),YTQueryFilter.NONE, request.getLimit())); + + for (QueryCache qc : queryCacheList){ + result.addAll(processQueryResult(qc)); + } + + for(MusicRecord mr : result){ + System.out.println(jaroWinkler.apply("asd","asd")); + log.trace(mr.getTitle() + (jaroWinkler.apply(mr.getTitle() != null ? mr.getTitle().toLowerCase() : "null", request.getQueryString().toLowerCase()))); + } + + return result; + /*return result.stream().sorted(Comparator.comparingDouble((MusicRecord m) -> + jaroWinkler.apply(m.getTitle() != null ? m.getTitle().toLowerCase() : "null", request.getQueryString().toLowerCase()) + )).collect(Collectors.toList());*/ + } + + private QueryCache performSearch(String queryString, YTQueryFilter filter, Integer limit){ + QueryCache qc = new QueryCache(queryString, filter, limit); Optional byQuery = queryCacheRepository.findById(qc.getId()); if(byQuery.isPresent()){ log.trace("Search query was found in cache"); - return processQueryResult(byQuery.get()); + return byQuery.get(); } - log.trace("Search query was NOT found in cache, running python script"); - String response = pythonRunner.query_py(request); - if(response.isBlank() || response.isEmpty()){ - throw new RuntimeException(); - } - qc.setResponse(response); + else{ + String response = pythonRunner.query_py(queryString,filter, limit); + if(response.isBlank()){ + throw new RuntimeException("python response is empty"); + } + qc.setResponse(response); - log.trace("Saving search query: {}",qc); - queryCacheRepository.save(qc); - return processQueryResult(qc); + log.trace("Saving search query: {}",qc); + return queryCacheRepository.save(qc); + } } + private List processQueryResult(QueryCache queryCache){ - Gson gson = new Gson(); - Type listType = new TypeToken>(){}.getType(); + try{ + Gson gson = new Gson(); + Type listType = new TypeToken>(){}.getType(); + + // Map the entire JSON hierarchy to your parent class + List musicRecordList = gson.fromJson(queryCache.getResponse(), listType); + musicRepository.saveAll(musicRecordList); + return musicRecordList; + } + catch (Exception e) { + e.printStackTrace(); + return new ArrayList<>(); + } - // Map the entire JSON hierarchy to your parent class - List musicRecordList = gson.fromJson(queryCache.getResponse(), listType); - System.out.println(musicRecordList); - musicRepository.saveAll(musicRecordList); - return musicRecordList; } }