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;
}
}