open api changes

This commit is contained in:
2026-08-04 10:35:08 +02:00
parent c23d8bbb90
commit 9d27fdd8f0
19 changed files with 175 additions and 48 deletions
+9
View File
@@ -11,6 +11,15 @@
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar" /> <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar" />
</processorPath> </processorPath>
<module name="musix" /> <module name="musix" />
</profile>
<profile name="Annotation profile for mpv-client" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar" />
</processorPath>
<module name="mpv-client" /> <module name="mpv-client" />
</profile> </profile>
</annotationProcessing> </annotationProcessing>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/musix/musix.iml" filepath="$PROJECT_DIR$/musix/musix.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+1 -1
View File
@@ -1,6 +1,6 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-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 { export interface Session {
sessionId: string; sessionId: string;
@@ -35,7 +35,7 @@ export class YoutubeSearchResult implements OnChanges {
this._album = this.musicRecord.album?.name || 'Album Name'; this._album = this.musicRecord.album?.name || 'Album Name';
this._duration = this.musicRecord.duration || '0:00'; this._duration = this.musicRecord.duration || '0:00';
this._views = this.musicRecord.views || '0 views'; 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;
} }
} }
@@ -3,6 +3,14 @@
<mat-label>Search</mat-label> <mat-label>Search</mat-label>
<input matInput [(ngModel)]="searchQuery" placeholder="Enter search query"> <input matInput [(ngModel)]="searchQuery" placeholder="Enter search query">
</mat-form-field> </mat-form-field>
<mat-form-field class="filter-field">
<mat-label>Filter</mat-label>
<mat-select [(ngModel)]="searchFilter">
<mat-option *ngFor="let f of filters" [value]="f">{{f}}</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" (click)="performSearch()">Search</button> <button mat-raised-button color="primary" (click)="performSearch()">Search</button>
</div> </div>
@@ -11,23 +11,47 @@ import { YoutubeSearchResult } from '../youtube-search-result/youtube-search-res
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@Component({ @Component({
selector: 'app-youtube-search-tab', 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', templateUrl: './youtube-search-tab.html',
styleUrl: './youtube-search-tab.css' styleUrl: './youtube-search-tab.css',
}) })
export class YoutubeSearchTab { export class YoutubeSearchTab {
private readonly youtubeService = inject(YoutubeControllerService); private readonly youtubeService = inject(YoutubeControllerService);
searchResults: WritableSignal<MusicRecord[]> = signal([]); searchResults: WritableSignal<MusicRecord[]> = signal([]);
searchQuery: string = ''; searchQuery: string = '';
searchFilter: QueryDto['filter'] = 'SONGS';
filters: QueryDto['filter'][] = [
'SONGS',
'VIDEOS',
'ALBUMS',
'ARTISTS',
'PLAYLISTS',
'COMMUNITY_PLAYLISTS',
'FEATURED_PLALISTS',
'PROFILES',
'PODCASTS',
'EPISODES',
'NONE',
];
performSearch(): void { performSearch(): void {
const queryDto: QueryDto = { const queryDto: QueryDto = {
queryString: this.searchQuery, queryString: this.searchQuery,
filter: this.searchFilter,
}; };
this.youtubeService.query(queryDto).subscribe({ this.youtubeService.query(queryDto).subscribe({
@@ -40,8 +64,5 @@ export class YoutubeSearchTab {
console.error('Query error:', error); console.error('Query error:', error);
}, },
}); });
} }
} }
+7 -2
View File
@@ -88,6 +88,11 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.5</version> <version>2.8.5</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -168,12 +173,12 @@
</plugins> </plugins>
</build> </build>
<repositories> <!--repositories>
<repository> <repository>
<id>rpi-nexus</id> <id>rpi-nexus</id>
<name>RPI Nexus proxy repository</name> <name>RPI Nexus proxy repository</name>
<url>http://192.168.0.187:7781/repository/maven-central/</url> <url>http://192.168.0.187:7781/repository/maven-central/</url>
</repository> </repository>
</repositories> <repositories-->
</project> </project>
+7 -1
View File
@@ -16,7 +16,13 @@ if(args.limit != None):
limit=args.limit; limit=args.limit;
if(args.filter != None): if(args.filter != None):
filter=args.filter filter=args.filter
tries = 1
ytmusic = YTMusic() ytmusic = YTMusic()
search_results = ytmusic.search(args.query, filter=filter, limit = limit) 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) print(search_results)
@@ -1,6 +1,8 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Description;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -18,6 +20,12 @@ public class ClientController {
@Autowired @Autowired
private ClientService clientService; 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") @PostMapping("/register")
public ResponseEntity<Client> registerClient(@RequestBody ClientRegistrationRequestDto clientRegistrationRequestDto){ public ResponseEntity<Client> registerClient(@RequestBody ClientRegistrationRequestDto clientRegistrationRequestDto){
return ResponseEntity.ok(clientService.register(clientRegistrationRequestDto)); return ResponseEntity.ok(clientService.register(clientRegistrationRequestDto));
@@ -1,5 +1,6 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; 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) @RequestMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE)
public class HealthController { 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") @GetMapping("/ping")
public String ping(){ public String ping(){
return "pong"; return "pong";
@@ -1,5 +1,6 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -17,6 +18,10 @@ public class PlayerController {
@Autowired @Autowired
private PlayerService playerService; 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}") @PostMapping("/play/{id}")
public ResponseEntity<String> play(@PathVariable String id){ public ResponseEntity<String> play(@PathVariable String id){
playerService.play(id); playerService.play(id);
@@ -1,5 +1,6 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -16,11 +17,21 @@ public class SessionController {
private SessionService sessionService; 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 @GetMapping
public ResponseEntity<Session> getSession(){ public ResponseEntity<Session> getSession(){
return ResponseEntity.ok(sessionService.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}") @PostMapping("/music/add/{musicId}")
public ResponseEntity<User> addMusic(@RequestHeader("Authorization") String userName, @PathVariable String musicId){ public ResponseEntity<User> addMusic(@RequestHeader("Authorization") String userName, @PathVariable String musicId){
return ResponseEntity.ok(sessionService.addMusicToQueue(userName,musicId)); return ResponseEntity.ok(sessionService.addMusicToQueue(userName,musicId));
@@ -1,5 +1,6 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -17,6 +18,12 @@ public class UserController {
@Autowired @Autowired
SessionService sessionService; 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 @PostMapping
public ResponseEntity<List<User>> createUser(@RequestBody CreateUserRequest userRequest) { public ResponseEntity<List<User>> createUser(@RequestBody CreateUserRequest userRequest) {
return ResponseEntity.ok(sessionService.addUser(userRequest)); return ResponseEntity.ok(sessionService.addUser(userRequest));
@@ -1,5 +1,6 @@
package zv.musix.controller; package zv.musix.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import zv.musix.dto.yt.QueryDto; import zv.musix.dto.yt.QueryDto;
import zv.musix.entity.QueryCache;
import zv.musix.python.json.MusicRecord; import zv.musix.python.json.MusicRecord;
import zv.musix.service.YtService; import zv.musix.service.YtService;
@@ -22,6 +22,14 @@ public class YoutubeController {
private YtService ytService; 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") @PostMapping("/query")
public ResponseEntity<List<MusicRecord>> query(@RequestBody QueryDto queryDto ) { public ResponseEntity<List<MusicRecord>> query(@RequestBody QueryDto queryDto ) {
return ResponseEntity.ok(ytService.query(queryDto)); return ResponseEntity.ok(ytService.query(queryDto));
@@ -6,7 +6,6 @@ import zv.musix.enums.YTQueryFilter;
@Data @Data
public class QueryDto { public class QueryDto {
String queryString; String queryString;
YTQueryFilter filter;
Integer limit; Integer limit;
} }
@@ -23,10 +23,10 @@ public class QueryCache {
@TimeToLive @TimeToLive
private Long timeoutInSeconds = 600L; private Long timeoutInSeconds = 600L;
public QueryCache(QueryDto dto){ public QueryCache(String query, YTQueryFilter filter, Integer limit ){
this.query = dto.getQueryString(); this.query = query;
this.filter = dto.getFilter() == null ? YTQueryFilter.SONGS : dto.getFilter(); this.filter = filter;
this.limit = dto.getLimit() == null ? 20 : dto.getLimit() ; this.limit = limit == null ? 20 : limit ;
this.id = query.hashCode() + filter.label + limit; this.id = query.hashCode() + filter.label + limit;
} }
@@ -45,19 +45,19 @@ public class PythonRunner {
} }
public String query_py(QueryDto request){ public String query_py(String query, YTQueryFilter filter, Integer limit){
ArrayList<String> args = new ArrayList<>(); ArrayList<String> args = new ArrayList<>();
args.add(PYTHON); args.add(PYTHON);
args.add(QUERY_PY); args.add(QUERY_PY);
args.add("--query"); args.add("--query");
args.add(request.getQueryString()); args.add(query);
if(request.getFilter() != null && request.getFilter() != YTQueryFilter.NONE){ if(filter != null && filter != YTQueryFilter.NONE){
args.add("--filter"); args.add("--filter");
args.add(request.getFilter().label); args.add(filter.label);
} }
if(request.getLimit() != null) { if(limit != null) {
args.add("--limit"); args.add("--limit");
args.add(request.getLimit().toString()); args.add(limit.toString());
} }
return runScript(args); return runScript(args);
} }
@@ -2,10 +2,12 @@ package zv.musix.service;
import com.google.gson.Gson; import com.google.gson.Gson;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.apache.commons.text.similarity.JaroWinklerDistance;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zv.musix.dto.yt.QueryDto; import zv.musix.dto.yt.QueryDto;
import zv.musix.entity.QueryCache; import zv.musix.entity.QueryCache;
import zv.musix.enums.YTQueryFilter;
import zv.musix.python.PythonRunner; import zv.musix.python.PythonRunner;
import zv.musix.python.json.MusicRecord; import zv.musix.python.json.MusicRecord;
import zv.musix.repository.MusicRepository; import zv.musix.repository.MusicRepository;
@@ -14,8 +16,11 @@ import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
@Service @Service
@Log4j2 @Log4j2
@@ -30,34 +35,66 @@ public class YtService {
public List<MusicRecord> query(QueryDto request){ public List<MusicRecord> query(QueryDto request){
JaroWinklerDistance jaroWinkler = new JaroWinklerDistance();
log.trace("Search query in cache: {}", request); log.trace("Search query in cache: {}", request);
QueryCache qc = new QueryCache(request); List<QueryCache> queryCacheList = new ArrayList<>();
List<MusicRecord> 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<QueryCache> byQuery = queryCacheRepository.findById(qc.getId()); Optional<QueryCache> byQuery = queryCacheRepository.findById(qc.getId());
if(byQuery.isPresent()){ if(byQuery.isPresent()){
log.trace("Search query was found in cache"); 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"); else{
String response = pythonRunner.query_py(request); String response = pythonRunner.query_py(queryString,filter, limit);
if(response.isBlank() || response.isEmpty()){ if(response.isBlank()){
throw new RuntimeException(); throw new RuntimeException("python response is empty");
} }
qc.setResponse(response); qc.setResponse(response);
log.trace("Saving search query: {}",qc); log.trace("Saving search query: {}",qc);
queryCacheRepository.save(qc); return queryCacheRepository.save(qc);
return processQueryResult(qc); }
} }
private List<MusicRecord> processQueryResult(QueryCache queryCache){ private List<MusicRecord> processQueryResult(QueryCache queryCache){
Gson gson = new Gson(); try{
Type listType = new TypeToken<List<MusicRecord>>(){}.getType(); Gson gson = new Gson();
Type listType = new TypeToken<List<MusicRecord>>(){}.getType();
// Map the entire JSON hierarchy to your parent class
List<MusicRecord> 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<MusicRecord> musicRecordList = gson.fromJson(queryCache.getResponse(), listType);
System.out.println(musicRecordList);
musicRepository.saveAll(musicRecordList);
return musicRecordList;
} }
} }