Compare commits

...

No commits in common. 'AddSong' and 'main' have entirely different histories.

3
.idea/.gitignore vendored

@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/音乐播放器项目.iml" filepath="$PROJECT_DIR$/.idea/音乐播放器项目.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 雷电影
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,36 +0,0 @@
# MusicPlayer
#### Description
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

@ -1,39 +1,2 @@
# MusicPlayer
# YueQingyinmusicplayer
#### 介绍
{**以下是 Gitee 平台说明,您可以替换此简介**
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN。专为开发者提供稳定、高效、安全的云端软件开发协作平台
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

@ -1,46 +0,0 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
### log ###
*.log
### db ###
*.db
*.sqlite

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -1,136 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.musicplayer</groupId>
<artifactId>musicplayer</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>21.0.2</javafx.version>
</properties>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- SQLite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
<!-- Jaudiotagger for reading audio metadata -->
<dependency>
<groupId>org</groupId>
<artifactId>jaudiotagger</artifactId>
<version>2.0.3</version>
</dependency>
<!-- 其他可能需要的依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<!-- 日志系统配置 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.4.14</version>
</dependency>
<!-- Jackson Core for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- 使用最新稳定版 -->
</dependency>
<!-- 确保Java Stream API可用 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Apache HttpClient (optional, if you need more HTTP features) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.musicPlayer.MusicPlayerApp</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -1,82 +0,0 @@
//package com.musicPlayer;
//
//import com.musicPlayer.controller.MusicPlayerController; // You might need to get controller instance
//import com.musicPlayer.util.FFmpegUtil;
//import javafx.application.Application;
//import javafx.application.Platform;
//import javafx.fxml.FXMLLoader;
//import javafx.scene.Parent;
//import javafx.scene.Scene;
//import javafx.stage.Stage;
//
//import java.io.IOException;
//import java.util.Objects;
//
//public class MusicPlayerApp extends Application {
//
// private MusicPlayerController controller; // To call cleanup
//
// @Override
// public void start(Stage primaryStage) throws Exception {
// // Test environment (consider doing this more gracefully or on demand)
// testEnvironment();
//
// // Load main interface
// FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(getClass().getResource("/fxml/MusicPlayer.fxml")));
// Parent root = loader.load();
// controller = loader.getController(); // Get controller instance
//
// primaryStage.setTitle("Music Player");
// primaryStage.setScene(new Scene(root, 800, 600));
// primaryStage.setOnCloseRequest(event -> {
// if (controller != null) {
// controller.cleanupPlayerBeforeExit();
// }
// Platform.exit(); // Ensure JavaFX toolkit terminates
// System.exit(0); // Force exit if needed
// });
// primaryStage.show();
// }
//
// private void testEnvironment() {
// // Test FFmpeg (this is a basic test, actual config status is in FFmpegUtil)
// // String ffmpegStatus = FFmpegUtil.getConfigStatus();
// // System.out.println("FFmpeg Status: " + ffmpegStatus);
// // Consider logging this instead of stdout or showing in an "About" or "Diagnostics" section.
// }
//
// @Override
// public void stop() throws Exception {
// // This method is called when the application is shutting down.
// if (controller != null) {
// controller.cleanupPlayerBeforeExit();
// }
// System.out.println("MusicPlayerApp stop method called. Application is closing.");
// super.stop(); // Important to call super
// }
//
// public static void main(String[] args) {
// launch(args);
// }
//}
package com.musicPlayer;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MusicPlayerApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/fxml/LoginView.fxml"));
primaryStage.setTitle("音乐播放器 - 登录");
primaryStage.setScene(new Scene(root, 400, 350));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

@ -1,73 +0,0 @@
package com.musicPlayer.controller;
import com.musicPlayer.services.UserService;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
public class LoginController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
@FXML private Button loginBtn;
@FXML private Hyperlink toRegisterLink;
private final UserService userService = new UserService();
@FXML
private void initialize() {
// 事件绑定由FXML onAction保证
}
@FXML
private void handleLogin(ActionEvent event) {
String username = usernameField.getText();
String password = passwordField.getText();
if (username.isEmpty() || password.isEmpty()) {
showAlert("请输入用户名和密码");
return;
}
if (userService.login(username, password)) {
// 登录成功,切换到主界面
try {
Stage stage = (Stage) loginBtn.getScene().getWindow();
Parent root = FXMLLoader.load(getClass().getResource("/fxml/MusicPlayer.fxml"));
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles/main.css").toExternalForm());
stage.setScene(scene);
} catch (Exception e) {
showAlert("无法加载主界面: " + e.getMessage());
}
} else {
showAlert("用户名或密码错误");
}
}
@FXML
private void handleToRegister(ActionEvent event) {
try {
Stage stage = (Stage) loginBtn.getScene().getWindow();
Parent root = FXMLLoader.load(getClass().getResource("/fxml/RegisterView.fxml"));
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles/main.css").toExternalForm());
stage.setScene(scene);
} catch (Exception e) {
showAlert("无法加载注册界面: " + e.getMessage());
}
}
private void showAlert(String msg) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("提示");
alert.setHeaderText(null);
alert.setContentText(msg);
alert.showAndWait();
}
}

@ -1,904 +0,0 @@
package com.musicPlayer.controller;
import java.io.File;
import java.io.IOException;
import java.io.ByteArrayInputStream; // For album art
import java.util.*;
import java.util.prefs.Preferences;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.musicPlayer.model.LyricLine;
import com.musicPlayer.model.Song;
import com.musicPlayer.services.MusicPlayerService;
import com.musicPlayer.services.RecommendationService;
import com.musicPlayer.util.FFmpegUtil;
import com.musicPlayer.util.LrcParser;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
public class MusicPlayerController {
private static final Logger logger = LoggerFactory.getLogger(MusicPlayerController.class);
@FXML private ListView<String> playlistView; // Assuming this is for future playlist features
@FXML private ListView<Song> songListView;
@FXML private Slider volumeSlider;
@FXML private Slider progressSlider;
@FXML private Label currentSongLabel;
@FXML private Label timeLabel;
@FXML private ImageView albumCoverView;
@FXML private Label artistLabel;
@FXML private Label albumLabel;
@FXML private ListView<LyricLine> lyricsView;
// New buttons
@FXML private Button previousButton;
@FXML private Button playPauseButton; // This will handle both play and pause
@FXML private Button nextButton;
@FXML private Button generateRecommendBtn;
private final MusicPlayerService musicPlayerService;
private MediaPlayer mediaPlayer;
private final DoubleProperty currentPlaybackTime = new SimpleDoubleProperty(0);
private Song currentlyPlayingSong; // Renamed from currentlySelectedSong for clarity in playback context
// private boolean isPaused = false; // We'll manage state directly via MediaPlayer.getStatus() more
@FXML private BorderPane rootPane;
private final Map<KeyCombination, Runnable> shortcutActions = new HashMap<>();
private boolean shortcutsEnabled = true;
private ObservableList<Song> observableSongList = FXCollections.observableArrayList();
private static final Set<String> SUPPORTED_FORMATS = Set.of("mp3", "wav", "aac", "mp4", "m4a", "aiff");
private RecommendationService recommendationService;
private ListView<Song> recommendationListView;
private static final String API_BASE_URL = "https://api.deepseek.com/v1";
private static final String CHAT_COMPLETION_PATH = "/chat/completions";
public MusicPlayerController() {
this.musicPlayerService = new MusicPlayerService();
}
@FXML
public void initialize() {
songListView.setItems(observableSongList);
songListView.setCellFactory(lv -> new ListCell<Song>() {
private final Button deleteButton = new Button();
private final Label textLabel = new Label();
private final HBox cellBox = new HBox();
{
// 设置垃圾桶图标
ImageView icon = new ImageView(new Image(getClass().getResourceAsStream("/images/delete.png")));
icon.setFitWidth(18);
icon.setFitHeight(18);
deleteButton.setGraphic(icon);
deleteButton.setStyle("-fx-background-color: transparent;");
deleteButton.getStyleClass().add("delete-btn");
deleteButton.setOnAction(e -> {
Song song = getItem();
if (song != null) {
handleDeleteSong(song);
}
e.consume();
});
textLabel.getStyleClass().add("song-label");
cellBox.getChildren().addAll(textLabel, deleteButton);
cellBox.setSpacing(10);
cellBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT);
deleteButton.setVisible(false);
this.setOnMouseEntered(e -> deleteButton.setVisible(true));
this.setOnMouseExited(e -> deleteButton.setVisible(false));
}
@Override
protected void updateItem(Song song, boolean empty) {
super.updateItem(song, empty);
if (empty || song == null) {
setText(null);
setGraphic(null);
} else {
textLabel.setText(formatSongDisplay(song));
setGraphic(cellBox);
}
}
});
// 设置歌词 ListView 的 CellFactory
lyricsView.setCellFactory(lv -> new ListCell<LyricLine>() {
@Override
protected void updateItem(LyricLine item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getText());
getStyleClass().removeAll("current-lyric-line");
if (isSelected()) {
getStyleClass().add("current-lyric-line");
}
}
}
});
songListView.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if (newSelection != null) {
lyricsView.getItems().clear();
}
});
songListView.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
Song selectedSong = songListView.getSelectionModel().getSelectedItem();
if (selectedSong != null) {
// 播放歌曲也用异步,防止大文件卡顿
new Thread(() -> playSong(selectedSong)).start();
}
}
});
// 音量滑块与MediaPlayer音量绑定
volumeSlider.valueProperty().addListener((obs, oldVal, newVal) -> {
if (mediaPlayer != null) {
mediaPlayer.setVolume(newVal.doubleValue());
}
});
// 异步加载歌曲列表
new Thread(this::loadSongsFromDatabase).start();
initializeSliders();
updatePlayPauseButtonState();
recommendationService = new RecommendationService();
setupRecommendationList();
// 在initialize方法末尾控制按钮可用性
if (generateRecommendBtn != null) {
generateRecommendBtn.setDisable(observableSongList.isEmpty());
}
}
private void loadSongsFromDatabase() {
try {
List<Song> songs = musicPlayerService.getAllSongsFromDB();
Platform.runLater(() -> {
observableSongList.setAll(songs);
if (!observableSongList.isEmpty()) {
songListView.getSelectionModel().selectFirst();
}
logger.info("Successfully loaded {} songs from database.", observableSongList.size());
updatePlayPauseButtonState();
});
} catch (Exception e) {
logger.error("Failed to load songs from database", e);
Platform.runLater(() -> {
showAlert("Database Error", "Could not load songs from the database. " + e.getMessage());
updatePlayPauseButtonState();
});
}
}
private void initializeSliders() {
volumeSlider.setValue(0.5);
progressSlider.setValue(0);
progressSlider.valueChangingProperty().addListener((obs, wasChanging, isChanging) -> {
if (!isChanging && mediaPlayer != null &&
(mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING || mediaPlayer.getStatus() == MediaPlayer.Status.PAUSED)) {
mediaPlayer.seek(Duration.seconds(progressSlider.getValue()));
}
});
progressSlider.setOnMouseClicked(event -> {
if (mediaPlayer != null &&
(mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING || mediaPlayer.getStatus() == MediaPlayer.Status.PAUSED)) {
double seekTime = (event.getX() / progressSlider.getWidth()) * progressSlider.getMax();
progressSlider.setValue(seekTime);
mediaPlayer.seek(Duration.seconds(seekTime));
}
});
}
public void handleAddSongs() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select Music Files");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Music Files", "*.mp3", "*.wav", "*.aac", "*.flac", "*.ogg"),
new FileChooser.ExtensionFilter("All Files", "*.*")
);
Stage stage = (Stage) songListView.getScene().getWindow();
List<File> selectedFiles = fileChooser.showOpenMultipleDialog(stage);
if (selectedFiles != null && !selectedFiles.isEmpty()) {
Task<List<Song>> addSongsTask = new Task<>() {
@Override
protected List<Song> call() throws Exception {
return musicPlayerService.addSongs(selectedFiles);
}
};
addSongsTask.setOnSucceeded(event -> {
List<Song> addedSongs = addSongsTask.getValue();
for (Song newSong : addedSongs) {
if (!observableSongList.contains(newSong)) {
observableSongList.add(newSong);
} else {
int index = observableSongList.indexOf(newSong);
if (index != -1) observableSongList.set(index, newSong);
}
}
if (!addedSongs.isEmpty()) songListView.getSelectionModel().select(observableSongList.get(observableSongList.size()-1));
logger.info("Added {} songs to the list.", addedSongs.size());
updatePlayPauseButtonState();
});
addSongsTask.setOnFailed(event -> {
logger.error("Failed to add songs", addSongsTask.getException());
showAlert("Error Adding Songs", "Could not process and add selected songs.");
});
new Thread(addSongsTask).start();
}
}
private String formatSongDisplay(Song song) {
if (song == null) return "";
return String.format("%s - %s (%s)",
song.getTitle(),
song.getArtist() != null ? song.getArtist() : "未知艺术家",
song.getFormattedDuration()); // 使用格式化方法
}
private String formatDuration(int milliseconds) {
int totalSeconds = milliseconds / 1000;
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%02d:%02d", minutes, seconds);
}
private void updateTimeLabel() {
if (mediaPlayer != null) {
Duration current = mediaPlayer.getCurrentTime();
Duration total = mediaPlayer.getTotalDuration();
String currentStr = formatDuration((int)current.toMillis());
String totalStr = formatDuration((int)total.toMillis());
timeLabel.setText(currentStr + " / " + totalStr);
}
}
private void updateSongInfo(Song song) {
if (song != null) {
currentSongLabel.setText(song.getTitle());
artistLabel.setText(song.getArtist() != null ? song.getArtist() : "未知艺术家");
albumLabel.setText(song.getAlbum() != null ? song.getAlbum() : "未知专辑");
// 更新专辑封面
updateAlbumCover(song);
} else {
currentSongLabel.setText("No song selected");
artistLabel.setText("未知艺术家");
albumLabel.setText("未知专辑");
albumCoverView.setImage(null);
}
}
private void updateAlbumCover(Song song) {
if (song != null) {
String expectedImageFileName = song.getTitle() + ".png"; // 根据歌曲标题构建文件名
String imagePath = "/images/" + expectedImageFileName; // 构建资源路径
try {
Image albumImage = new Image(getClass().getResourceAsStream(imagePath));
if (albumImage.isError()) {
// 如果加载的图片出错,可能是文件不存在或格式问题
logger.warn("Failed to load image from resource: {}", imagePath);
displayDefaultAlbumCover();
} else {
albumCoverView.setImage(albumImage);
logger.info("Displayed album cover from resource: {}", imagePath);
}
} catch (Exception e) {
// 捕获其他可能的异常,如 NullPointerException (如果资源路径或文件有问题)
logger.error("Error loading image resource: {}", imagePath, e);
displayDefaultAlbumCover();
}
} else {
// 如果歌曲对象为 null显示默认封面
displayDefaultAlbumCover();
}
}
// 新增方法:显示默认专辑封面
private void displayDefaultAlbumCover() {
try {
Image defaultCover = new Image(getClass().getResourceAsStream("/images/default-cover.png"));
albumCoverView.setImage(defaultCover);
} catch (Exception e) {
logger.error("Failed to load default cover image", e);
albumCoverView.setImage(null);
}
}
// 自动转码为mp3
private File getPlayableFile(File originalFile) {
String name = originalFile.getName();
String ext = name.substring(name.lastIndexOf('.') + 1).toLowerCase();
if (SUPPORTED_FORMATS.contains(ext)) {
return originalFile;
} else {
try {
File mp3File = FFmpegUtil.transcodeToMp3(originalFile);
System.out.println("【转码】已转码为: " + mp3File.getAbsolutePath());
return mp3File;
} catch (Exception e) {
e.printStackTrace();
showAlert("播放错误", "无法播放该格式或转码失败!");
return null;
}
}
}
private void playSong(Song song) {
if (song == null) return;
stopPlaybackClearUI();
currentlyPlayingSong = song;
updateSongInfo(song);
// 歌词调试代码
String audioPath = song.getFilePath();
String lrcPath = audioPath.substring(0, audioPath.lastIndexOf('.')) + ".lrc";
System.out.println("【歌词调试】音频路径: " + audioPath);
System.out.println("【歌词调试】歌词路径: " + lrcPath);
File lrcFile = new File(lrcPath);
System.out.println("【歌词调试】歌词文件是否存在: " + lrcFile.exists());
if (lrcFile.exists()) {
try {
List<LyricLine> lyrics = LrcParser.parse(lrcFile);
System.out.println("【歌词调试】歌词行数: " + lyrics.size());
for (LyricLine line : lyrics) {
System.out.println("【歌词调试】" + line.getTimestamp().toSeconds() + " : " + line.getText());
}
song.setLyrics(lyrics);
} catch (Exception e) {
e.printStackTrace();
song.setLyrics(new ArrayList<>());
}
} else {
System.out.println("【歌词调试】未找到歌词文件!");
song.setLyrics(new ArrayList<>());
}
// 清空并加载新歌词
lyricsView.getItems().clear();
if (song.getLyrics() != null && !song.getLyrics().isEmpty()) {
lyricsView.getItems().addAll(song.getLyrics());
}
try {
File songFile = new File(song.getFilePath());
File playableFile = getPlayableFile(songFile);
if (playableFile == null || !playableFile.exists()) {
showAlert("File Not Found", "The song file could not be found or is not playable: " + song.getFilePath());
logger.error("Song file not found or not playable: {}", song.getFilePath());
observableSongList.remove(song);
currentlyPlayingSong = null;
updatePlayPauseButtonState();
return;
}
Media media = new Media(playableFile.toURI().toString());
mediaPlayer = new MediaPlayer(media);
// 播放新歌时同步设置音量
mediaPlayer.setVolume(volumeSlider.getValue());
mediaPlayer.statusProperty().addListener((obs, oldStatus, newStatus) -> {
Platform.runLater(this::updatePlayPauseButtonState);
});
mediaPlayer.setOnReady(() -> {
Duration duration = media.getDuration();
progressSlider.setMax(duration.toSeconds());
updateTimeLabel();
mediaPlayer.currentTimeProperty().addListener((obs, oldTime, newTime) -> {
if (!progressSlider.isValueChanging()) {
progressSlider.setValue(newTime.toSeconds());
}
updateTimeLabel();
updateLyricsDisplay(newTime);
});
});
mediaPlayer.setOnEndOfMedia(() -> {
handleNextSong(null);
});
mediaPlayer.play();
updatePlayPauseButtonState();
updateRecommendations();
} catch (Exception e) {
logger.error("Error playing song: {}", song.getTitle(), e);
showAlert("Playback Error", "Could not play the selected song: " + e.getMessage());
}
}
@FXML
public void handlePlayPause(ActionEvent event) {
if (mediaPlayer != null) {
MediaPlayer.Status status = mediaPlayer.getStatus();
if (status == MediaPlayer.Status.PLAYING) {
mediaPlayer.pause();
currentSongLabel.setText("Paused: " + formatSongDisplay(currentlyPlayingSong));
logger.info("Paused: {}", currentlyPlayingSong.getTitle());
} else if (status == MediaPlayer.Status.PAUSED || status == MediaPlayer.Status.STOPPED || status == MediaPlayer.Status.READY) {
// If paused, or stopped/ready (meaning a song is loaded but not playing), play it.
// This also covers the case where a song ended and handleNextSong wasn't called or failed,
// or if playSong was called but didn't auto-play.
mediaPlayer.play();
currentSongLabel.setText("Playing: " + formatSongDisplay(currentlyPlayingSong));
logger.info("Resumed/Started playing: {}", currentlyPlayingSong.getTitle());
} else {
// Status might be UNKNOWN, STALLED, HALTED. Try to play selected song.
Song selectedSong = songListView.getSelectionModel().getSelectedItem();
if (selectedSong == null && !observableSongList.isEmpty()) {
songListView.getSelectionModel().selectFirst();
selectedSong = songListView.getSelectionModel().getSelectedItem();
}
if (selectedSong != null) {
playSong(selectedSong);
} else {
showAlert("No Song", "No song selected or playlist is empty.");
}
}
} else {
// No media player active, try to play the selected song or the first song.
Song selectedSong = songListView.getSelectionModel().getSelectedItem();
if (selectedSong == null && !observableSongList.isEmpty()) {
songListView.getSelectionModel().selectFirst();
selectedSong = songListView.getSelectionModel().getSelectedItem();
}
if (selectedSong != null) {
playSong(selectedSong);
} else {
showAlert("No Song", "No song to play. Playlist is empty.");
}
}
updatePlayPauseButtonState();
}
@FXML
public void handlePreviousSong(ActionEvent event) {
if (observableSongList.isEmpty()) return;
int currentIndex = songListView.getSelectionModel().getSelectedIndex();
if (currentIndex == -1 && currentlyPlayingSong != null) { // If a song was playing but not via list selection
currentIndex = observableSongList.indexOf(currentlyPlayingSong);
}
int previousIndex = currentIndex - 1;
if (previousIndex < 0) {
previousIndex = observableSongList.size() - 1; // Wrap around to the last song
}
songListView.getSelectionModel().select(previousIndex);
Song songToPlay = songListView.getSelectionModel().getSelectedItem();
if (songToPlay != null) {
playSong(songToPlay);
}
updateRecommendations();
}
@FXML
public void handleNextSong(ActionEvent event) {
if (observableSongList.isEmpty()) return;
int currentIndex = songListView.getSelectionModel().getSelectedIndex();
if (currentIndex == -1 && currentlyPlayingSong != null) { // If a song was playing but not via list selection
currentIndex = observableSongList.indexOf(currentlyPlayingSong);
}
int nextIndex = currentIndex + 1;
if (nextIndex >= observableSongList.size()) {
nextIndex = 0; // Wrap around to the first song
}
songListView.getSelectionModel().select(nextIndex);
Song songToPlay = songListView.getSelectionModel().getSelectedItem();
if (songToPlay != null) {
playSong(songToPlay);
}
updateRecommendations();
}
// Utility to stop playback and clear related UI elements
private void stopPlaybackClearUI() {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.dispose();
mediaPlayer = null;
}
currentlyPlayingSong = null;
progressSlider.setValue(0);
currentPlaybackTime.set(0);
timeLabel.setText("00:00 / 00:00");
updateSongInfo(null);
updatePlayPauseButtonState();
lyricsView.getItems().clear(); // 清空歌词显示
albumCoverView.setImage(null);
displayDefaultAlbumCover(); // 确保显示默认封面
}
// Update the text of the Play/Pause button based on the MediaPlayer state
private void updatePlayPauseButtonState() {
if (playPauseButton == null) return; // Not yet initialized
boolean listIsEmpty = observableSongList.isEmpty();
if (listIsEmpty) {
// No songs in the playlist
playPauseButton.setText("播放"); // Play
playPauseButton.setDisable(true); // Disable when no songs
} else if (mediaPlayer != null && mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) {
// Song is playing, button should trigger pause
playPauseButton.setText("暂停"); // Pause
playPauseButton.setDisable(false); // Enable button
} else {
// Songs available but not playing (either no mediaPlayer or paused/stopped/ready)
playPauseButton.setText("播放"); // Play
playPauseButton.setDisable(false); // Enable button
}
// Disable/enable next/prev buttons if list is empty
if (previousButton != null) previousButton.setDisable(listIsEmpty);
if (nextButton != null) nextButton.setDisable(listIsEmpty);
}
public void handleExit(ActionEvent actionEvent) {
logger.info("Exit action triggered. Cleaning up.");
cleanupPlayerBeforeExit();
Platform.exit();
System.exit(0);
}
private void showAlert(String title, String content) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(content);
alert.showAndWait();
});
}
public void cleanupPlayerBeforeExit() {
logger.info("Cleanup called before exit.");
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.dispose();
mediaPlayer = null;
logger.info("MediaPlayer disposed during cleanup.");
}
}
private String formatDuration(Duration duration) {
int minutes = (int) duration.toMinutes();
int seconds = (int) (duration.toSeconds() % 60);
return String.format("%02d:%02d", minutes, seconds);
}
@FXML
private void handleSettings(ActionEvent event) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/SettingsView.fxml"));
Parent settingsRoot = loader.load();
// 安全获取主窗口
Stage primaryStage = (Stage) ((MenuItem) event.getSource()).getParentPopup().getOwnerWindow();
Stage settingsStage = new Stage();
settingsStage.setTitle("设置");
settingsStage.setScene(new Scene(settingsRoot));
settingsStage.initOwner(primaryStage);
settingsStage.initModality(Modality.APPLICATION_MODAL);
settingsStage.showAndWait();
} catch (IOException e) {
e.printStackTrace();
showAlert("错误", "无法加载设置界面");
}
}
// 新增方法:根据当前时间更新歌词显示
private void updateLyricsDisplay(Duration currentTime) {
if (currentlyPlayingSong == null || currentlyPlayingSong.getLyrics() == null || currentlyPlayingSong.getLyrics().isEmpty()) {
return;
}
List<LyricLine> lyrics = currentlyPlayingSong.getLyrics();
int currentLineIndex = -1;
// 查找当前时间对应的歌词行
for (int i = 0; i < lyrics.size(); i++) {
if (currentTime.greaterThanOrEqualTo(lyrics.get(i).getTimestamp())) {
// 如果不是最后一行,并且当前时间小于下一行的时间戳,那么当前行就是这一行
if (i < lyrics.size() - 1 && currentTime.lessThan(lyrics.get(i + 1).getTimestamp())) {
currentLineIndex = i;
break;
} else if (i == lyrics.size() - 1) {
// 如果是最后一行
currentLineIndex = i;
break;
}
}
}
// 更新 ListView 的选中状态和滚动位置
if (currentLineIndex != -1) {
// 取消之前的选中状态
lyricsView.getSelectionModel().clearSelection();
// 选中当前歌词行
lyricsView.getSelectionModel().select(currentLineIndex);
// 滚动到当前歌词行,使其在视图中可见
lyricsView.scrollTo(currentLineIndex);
}
}
public void handleDeleteSong(Song song) {
// 1. 从数据库删除
musicPlayerService.deleteSong(song);
// 2. 从列表移除
observableSongList.remove(song);
// 3. 如果正在播放,停止播放
if (currentlyPlayingSong != null && currentlyPlayingSong.equals(song)) {
stopPlaybackClearUI();
}
}
private void setupRecommendationList() {
recommendationListView = new ListView<>();
recommendationListView.setCellFactory(lv -> new ListCell<Song>() {
@Override
protected void updateItem(Song song, boolean empty) {
super.updateItem(song, empty);
if (empty || song == null) {
setText(null);
} else {
setText(song.getTitle() + " - " + song.getArtist());
}
}
});
recommendationListView.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) {
Song selectedSong = recommendationListView.getSelectionModel().getSelectedItem();
if (selectedSong != null) {
playSong(selectedSong);
}
}
});
}
private void updateRecommendations() {
if (currentlyPlayingSong != null) {
String userId = "user123"; // 这里应该使用实际的用户ID
List<Song> recommendations = recommendationService.getPersonalizedRecommendations(userId, currentlyPlayingSong);
Platform.runLater(() -> {
recommendationListView.getItems().clear();
recommendationListView.getItems().addAll(recommendations);
});
}
}
@FXML
private void handleGenerateRecommendRequest(ActionEvent event) {
String song = (currentlyPlayingSong != null && currentlyPlayingSong.getTitle() != null) ? currentlyPlayingSong.getTitle() : "(无)";
String artist = (currentlyPlayingSong != null && currentlyPlayingSong.getArtist() != null) ? currentlyPlayingSong.getArtist() : "(无)";
// 异步调用避免阻塞UI
new Thread(() -> {
String result = musicPlayerService.getRecommendationFromDeepSeek(song, artist);
javafx.application.Platform.runLater(() -> {
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.INFORMATION);
alert.setTitle("AI音乐推荐");
alert.setHeaderText("推荐结果");
alert.setContentText(result);
alert.showAndWait();
});
}).start();
}
@FXML
// public void handleExit(ActionEvent actionEvent) { // From FXML menu
// logger.info("Exit action triggered from menu. Cleaning up.");
// cleanupPlayerBeforeExit();
// Platform.exit();
// System.exit(0); // Force exit if JavaFX doesn't terminate cleanly
// }
private void setupKeyListeners() {
rootPane.setOnKeyPressed(this::handleKeyPressed);
}
private void loadShortcuts() {
Preferences prefs = Preferences.userNodeForPackage(getClass());
boolean enabled = prefs.getBoolean("shortcuts.enabled", true);
setShortcutsEnabled(enabled);
// 默认快捷键
Map<String, String> defaultShortcuts = new HashMap<>();
defaultShortcuts.put("播放/暂停", "Ctrl+Shift+F5");
defaultShortcuts.put("停止", "Ctrl+Shift+F6");
defaultShortcuts.put("快进", "Ctrl+Shift+F8");
defaultShortcuts.put("快退", "Ctrl+Shift+F7");
defaultShortcuts.put("上一曲", "Ctrl+Shift+Left");
defaultShortcuts.put("下一曲", "Ctrl+Shift+Right");
defaultShortcuts.put("增大音量", "Ctrl+Shift+Up");
defaultShortcuts.put("减小音量", "Ctrl+Shift+Down");
defaultShortcuts.put("退出", "");
defaultShortcuts.put("显示/隐藏播放器", "");
defaultShortcuts.put("显示/隐藏桌面歌词", "");
defaultShortcuts.put("添加到我喜欢的音乐", "");
// 从首选项加载或使用默认值
Map<String, String> shortcutMap = new HashMap<>();
defaultShortcuts.forEach((function, defaultValue) -> {
String shortcut = prefs.get("shortcut." + function, defaultValue);
shortcutMap.put(function, shortcut);
});
initializeShortcuts(shortcutMap);
}
public void initializeShortcuts(Map<String, String> shortcutMap) {
shortcutActions.clear();
shortcutMap.forEach((function, shortcut) -> {
if (!shortcut.isEmpty()) {
try {
KeyCombination keyCombination = KeyCodeCombination.keyCombination(shortcut);
switch (function) {
case "播放/暂停":
shortcutActions.put(keyCombination, this::handlePlayPause);
break;
case "停止":
shortcutActions.put(keyCombination, this::handleStop);
break;
case "快进":
shortcutActions.put(keyCombination, this::handleFastForward);
break;
case "快退":
shortcutActions.put(keyCombination, this::handleRewind);
break;
case "上一曲":
shortcutActions.put(keyCombination, this::handlePreviousSong);
break;
case "下一曲":
shortcutActions.put(keyCombination, this::handleNextSong);
break;
case "增大音量":
shortcutActions.put(keyCombination, this::handleVolumeUp);
break;
case "减小音量":
shortcutActions.put(keyCombination, this::handleVolumeDown);
break;
case "退出":
shortcutActions.put(keyCombination, this::handleExit);
break;
case "显示/隐藏播放器":
shortcutActions.put(keyCombination, this::handleTogglePlayer);
break;
case "显示/隐藏桌面歌词":
shortcutActions.put(keyCombination, this::handleToggleLyrics);
break;
case "添加到我喜欢的音乐":
shortcutActions.put(keyCombination, this::handleAddToFavorites);
break;
}
} catch (IllegalArgumentException e) {
System.err.println("无效的快捷键组合: " + shortcut + " 功能: " + function);
}
}
});
}
private void handleKeyPressed(KeyEvent event) {
if (!shortcutsEnabled) return;
shortcutActions.forEach((keyCombination, action) -> {
if (keyCombination.match(event)) {
action.run();
event.consume();
}
});
}
public void setShortcutsEnabled(boolean enabled) {
this.shortcutsEnabled = enabled;
Preferences prefs = Preferences.userNodeForPackage(getClass());
prefs.putBoolean("shortcuts.enabled", enabled);
}
// 各个快捷键对应的处理方法
@FXML private void handlePlayPause() {
System.out.println("播放/暂停");
// 实际实现代码
}
@FXML private void handleStop() {
System.out.println("停止播放");
// 实际实现代码
}
@FXML private void handleFastForward() {
System.out.println("快进");
}
@FXML private void handleRewind() {
System.out.println("快退");
}
@FXML private void handlePreviousSong() {
System.out.println("上一曲");
}
@FXML private void handleNextSong() {
System.out.println("下一曲");
}
@FXML private void handleVolumeUp() {
System.out.println("增大音量");
}
@FXML private void handleVolumeDown() {
System.out.println("减小音量");
}
@FXML private void handleExit() {
System.out.println("退出");
// Platform.exit();
}
@FXML private void handleTogglePlayer() {
System.out.println("显示/隐藏播放器");
}
@FXML private void handleToggleLyrics() {
System.out.println("显示/隐藏桌面歌词");
}
@FXML private void handleAddToFavorites() {
System.out.println("添加到喜欢");
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.controller;
public class PlaylistController {
}

@ -1,75 +0,0 @@
package com.musicPlayer.controller;
import com.musicPlayer.services.UserService;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
public class RegisterController {
@FXML private TextField usernameField;
@FXML private PasswordField passwordField;
@FXML private PasswordField confirmPasswordField;
@FXML private Button registerBtn;
@FXML private Hyperlink toLoginLink;
private final UserService userService = new UserService();
@FXML
private void initialize() {
// 事件绑定由FXML onAction保证
}
@FXML
private void handleRegister(ActionEvent event) {
String username = usernameField.getText();
String password = passwordField.getText();
String confirmPassword = confirmPasswordField.getText();
if (username.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) {
showAlert("请填写所有字段");
return;
}
if (!password.equals(confirmPassword)) {
showAlert("两次输入的密码不一致");
return;
}
if (userService.userExists(username)) {
showAlert("用户名已存在,请更换");
return;
}
if (userService.register(username, password)) {
showAlert("注册成功,请登录!");
handleToLogin(event);
} else {
showAlert("注册失败,请重试");
}
}
@FXML
private void handleToLogin(ActionEvent event) {
try {
Stage stage = (Stage) registerBtn.getScene().getWindow();
Parent root = FXMLLoader.load(getClass().getResource("/fxml/LoginView.fxml"));
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/styles/main.css").toExternalForm());
stage.setScene(scene);
} catch (Exception e) {
showAlert("无法加载登录界面: " + e.getMessage());
}
}
private void showAlert(String msg) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("提示");
alert.setHeaderText(null);
alert.setContentText(msg);
alert.showAndWait();
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.controller;
public class SearchController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.controller;
public class SettingsController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.controller;
public class TimerController {
}

@ -1,26 +0,0 @@
package com.musicPlayer.model;
import javafx.util.Duration;
public class LyricLine {
private final Duration timestamp; // 歌词开始显示的时间戳
private final String text; // 歌词文本
public LyricLine(Duration timestamp, String text) {
this.timestamp = timestamp;
this.text = text;
}
public Duration getTimestamp() {
return timestamp;
}
public String getText() {
return text;
}
@Override
public String toString() {
return "[" + timestamp + "] " + text;
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.model;
public class Playlist {
}

@ -1,4 +0,0 @@
package com.musicPlayer.model;
public class Settings {
}

@ -1,109 +0,0 @@
package com.musicPlayer.model;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Song {
private int id;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty artist = new SimpleStringProperty();
private final StringProperty path = new SimpleStringProperty();
private final StringProperty lrcPath = new SimpleStringProperty();
private final StringProperty genre = new SimpleStringProperty();
private final StringProperty album = new SimpleStringProperty();
private final StringProperty year = new SimpleStringProperty();
private double similarityScore; // 用于推荐系统的相似度分数
private String filePath;
private int duration; // 以毫秒为单位存储duration更精确
private List<LyricLine> lyrics; // 修改:存储歌词行的列表
private byte[] albumArtData;
public Song(String title, String artist, String album, String filePath, int duration) {
this.title.set(title);
this.artist.set(artist);
this.album.set(album);
this.filePath = filePath;
this.duration = duration;
this.lyrics = new ArrayList<>(); // 初始化歌词列表
this.albumArtData = null; }
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getTitle() { return title.get(); }
public void setTitle(String title) { this.title.set(title); }
public StringProperty titleProperty() { return title; }
public String getArtist() { return artist.get(); }
public void setArtist(String artist) { this.artist.set(artist); }
public StringProperty artistProperty() { return artist; }
public String getPath() { return path.get(); }
public void setPath(String path) { this.path.set(path); }
public StringProperty pathProperty() { return path; }
public String getLrcPath() { return lrcPath.get(); }
public void setLrcPath(String lrcPath) { this.lrcPath.set(lrcPath); }
public StringProperty lrcPathProperty() { return lrcPath; }
public String getGenre() { return genre.get(); }
public void setGenre(String genre) { this.genre.set(genre); }
public StringProperty genreProperty() { return genre; }
public String getAlbum() { return album.get(); }
public void setAlbum(String album) { this.album.set(album); }
public StringProperty albumProperty() { return album; }
public String getYear() { return year.get(); }
public void setYear(String year) { this.year.set(year); }
public StringProperty yearProperty() { return year; }
public double getSimilarityScore() { return similarityScore; }
public void setSimilarityScore(double similarityScore) { this.similarityScore = similarityScore; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
// 获取毫秒值
public int getDuration() {
return duration;
}
// 设置毫秒值
public void setDuration(int duration) {
this.duration = duration;
}
// 获取秒数(计算值)
public int getDurationInSeconds() {
return duration / 1000;
}
// 获取格式化字符串UI使用
public String getFormattedDuration() {
int totalSeconds = getDurationInSeconds();
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%d:%02d", minutes, seconds);
}
// 获取和设置歌词列表
public List<LyricLine> getLyrics() { return lyrics; }
public void setLyrics(List<LyricLine> lyrics) { this.lyrics = lyrics; }
@Override
public String toString() {
return title.get() + " - " + artist.get();
}
public byte[] getAlbumArtData() {
return albumArtData;
}
public void setAlbumArtData(byte[] albumArtData) {
this.albumArtData = albumArtData;
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.model;
public class User {
}

@ -1,73 +0,0 @@
package com.musicPlayer.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.musicPlayer.model.Song;
import com.musicPlayer.util.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ApiService {
private static final Logger logger = LoggerFactory.getLogger(ApiService.class);
// 修正 1使用环境变量获取 API Key
private static final String API_KEY = System.getenv("DEEPSEEK_API_KEY");
// 修正 2只保留基础 URL
private static final String API_BASE_URL = "https://api.deepseek.com/v1";
private static final Map<String, String> DEFAULT_HEADERS = new HashMap<>();
static {
DEFAULT_HEADERS.put("Content-Type", "application/json");
// 修正 3安全地使用 API Key
if (API_KEY != null) {
DEFAULT_HEADERS.put("Authorization", "Bearer " + API_KEY);
}
}
// 新增:通用聊天 API 方法
public JsonNode chatCompletion(List<Map<String, String>> messages)
throws IOException, InterruptedException {
String url = API_BASE_URL + "/chat/completions";
Map<String, Object> body = new HashMap<>();
body.put("model", "deepseek-chat");
body.put("messages", messages);
body.put("stream", false);
String response = HttpUtil.post(url, DEFAULT_HEADERS, HttpUtil.toJson(body));
return HttpUtil.fromJson(response, JsonNode.class);
}
// 修改:使用通用聊天 API 获取推荐
public JsonNode getRecommendations(String userId, Song currentSong)
throws IOException, InterruptedException {
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", "你是一个音乐推荐助手"));
String prompt = String.format("用户ID%s 正在收听:%s - %s。推荐10首相似歌曲用JSON数组返回包含title,artist,genre字段",
userId, currentSong.getTitle(), currentSong.getArtist());
messages.add(Map.of("role", "user", "content", prompt));
return chatCompletion(messages);
}
// 修改:使用通用聊天 API 生成歌词
public String getAiGeneratedLyrics(String songTitle, String artist)
throws IOException, InterruptedException {
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", "你是一个歌词创作助手"));
String prompt = String.format("为歌曲《%s》- %s 生成歌词,风格类似原艺术家", songTitle, artist);
messages.add(Map.of("role", "user", "content", prompt));
JsonNode response = chatCompletion(messages);
return response.get("choices").get(0).get("message").get("content").asText();
}
// 删除不需要的方法(如 updateUserPreferences
}

@ -1,4 +0,0 @@
package com.musicPlayer.services;
public class LoginService {
}

@ -1,153 +0,0 @@
package com.musicPlayer.services;
import com.musicPlayer.util.ConfigUtil;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.musicPlayer.util.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.musicPlayer.model.LyricLine;
import com.musicPlayer.model.Song;
import com.musicPlayer.util.DBUtil;
import com.musicPlayer.util.FFmpegUtil;
import com.musicPlayer.util.LrcParser;
public class MusicPlayerService {
private final DBUtil dbUtil;
private static final Logger logger = LoggerFactory.getLogger(MusicPlayerService.class);
public MusicPlayerService() {
this.dbUtil = new DBUtil();
}
public Song getSongAt(int index) {
try {
List<Song> songs = dbUtil.getAllSongs();
if (index >= 0 && index < songs.size()) {
return songs.get(index);
}
return null;
} catch (Exception e) {
logger.error("Error getting song at index {}", index, e);
return null;
}
}
public List<Song> addSongs(List<File> files) {
return files.stream()
.map(file -> {
// Check if the song already exists
Song existingSong = dbUtil.findSongByPath(file.getAbsolutePath());
if (existingSong != null) {
logger.info("Song already exists in DB: {}", file.getAbsolutePath());
return existingSong;
}
// 自动生成lrc如果没有
String audioFilePath = file.getAbsolutePath();
String lrcPath = audioFilePath.substring(0, audioFilePath.lastIndexOf('.')) + ".lrc";
File lrcFile = new File(lrcPath);
if (!lrcFile.exists()) {
// 尝试查找同名txt或无标签歌词
String txtPath = audioFilePath.substring(0, audioFilePath.lastIndexOf('.')) + ".txt";
File txtFile = new File(txtPath);
if (txtFile.exists()) {
try {
LrcParser.autoGenerateLrcWithTimestamps(file, txtFile, 5); // 每行5秒
logger.info("自动生成lrc: {}", lrcFile.getAbsolutePath());
} catch (Exception e) {
logger.error("自动生成lrc失败: {}", lrcFile.getAbsolutePath(), e);
}
}
}
// Parse music file metadata (using FFmpegUtil for basic info)
Song song = FFmpegUtil.extractMetadata(file);
// Parse lyrics file
List<LyricLine> lyrics = parseLyricsFromFile(file);
song.setLyrics(lyrics); // Set lyrics to the song object
// Save to database
dbUtil.saveSong(song);
logger.info("Added song to DB: {}", song.getTitle());
return song;
})
.collect(Collectors.toList());
}
// 修改方法:解析歌词文件,返回 LyricLine 列表
private List<LyricLine> parseLyricsFromFile(File audioFile) {
String audioFilePath = audioFile.getAbsolutePath();
String lyricsFilePath = audioFilePath.substring(0, audioFilePath.lastIndexOf('.')) + ".lrc";
File lyricsFile = new File(lyricsFilePath);
try {
return LrcParser.parse(lyricsFile); // 调用 LrcParser 进行解析
} catch (IOException e) {
logger.error("Error parsing lyrics file: {}", lyricsFilePath, e);
return new ArrayList<>(); // 解析失败返回空列表
}
}
/**
* Retrieves all songs from the database.
* @return A list of all songs, or an empty list if an error occurs.
*/
public List<Song> getAllSongsFromDB() {
// Assuming DBUtil.getAllSongs fetches lyrics as well
return dbUtil.getAllSongs();
}
public void deleteSong(Song song) {
if (song == null) return;
dbUtil.deleteSong(song);
logger.info("Deleted song from DB: {}", song.getTitle());
}
public String getDeepSeekRecommendation(String apiKey, String userPrompt) throws IOException, InterruptedException {
String url = "https://api.deepseek.com/v1/chat/completions";
Map<String, Object> body = new HashMap<>();
body.put("model", "deepseek-chat");
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of("role", "system", "content", "你是一个音乐推荐助手。"));
messages.add(Map.of("role", "user", "content", userPrompt));
body.put("messages", messages);
body.put("stream", false);
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Authorization", "Bearer " + apiKey);
String response = HttpUtil.post(url, headers, HttpUtil.toJson(body));
// 解析返回的 assistant 内容
com.fasterxml.jackson.databind.JsonNode json = HttpUtil.fromJson(response, com.fasterxml.jackson.databind.JsonNode.class);
return json.get("choices").get(0).get("message").get("content").asText();
}
public String getRecommendationFromDeepSeek(String song, String artist) {
String apiKey = ConfigUtil.get("deepseek.api.key");
// 调试输出API Key只显示前后几位避免泄露
if (apiKey != null && !apiKey.isEmpty()) {
String safeKey = apiKey.length() > 10 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4) : apiKey;
System.out.println("[调试] 读取到的API Key: " + safeKey);
} else {
System.out.println("[调试] 未读取到API Key");
}
if (apiKey == null || apiKey.isEmpty()) {
return "未检测到 DeepSeek API Key请在 config.properties 中配置 deepseek.api.key";
}
String prompt = "你是一个音乐推荐助手,请根据我正在听的歌曲:" + song + " 和歌手:" + artist + ",推荐几首类似的歌曲";
try {
return getDeepSeekRecommendation(apiKey, prompt);
} catch (Exception e) {
logger.error("获取推荐失败", e);
return "推荐获取失败:" + e.getMessage();
}
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.services;
public class PlaylistService {
}

@ -1,146 +0,0 @@
package com.musicPlayer.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.musicPlayer.model.Song;
import com.musicPlayer.util.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
public class RecommendationService {
private static final Logger logger = LoggerFactory.getLogger(RecommendationService.class);
private static final String RECOMMENDATION_API_URL = "https://api.deepseek.com/v1"; // 替换为实际的API地址
private final ApiService apiService;
private final Map<String, List<Song>> userRecommendations = new HashMap<>();
private final Map<String, Long> lastUpdateTime = new HashMap<>();
private static final long CACHE_DURATION = 3600000; // 1小时的缓存时间
public RecommendationService() {
this.apiService = new ApiService();
}
/**
*
* @param userId ID
* @param currentSong
* @return
*/
public List<Song> getPersonalizedRecommendations(String userId, Song currentSong) {
// 修正 1使用歌曲信息创建缓存键
String cacheKey = userId + "-" + currentSong.getId();
// ====== 调试输出API Key ======
String apiKey = com.musicPlayer.util.ConfigUtil.get("deepseek.api.key");
if (apiKey != null && !apiKey.isEmpty()) {
String safeKey = apiKey.length() > 10 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4) : apiKey;
System.out.println("[调试] RecommendationService 读取到的API Key: " + safeKey);
} else {
System.out.println("[调试] RecommendationService 未读取到API Key");
}
// ====== End ======
try {
// 检查缓存
if (isCacheValid(cacheKey)) {
return userRecommendations.get(cacheKey);
}
// 修正 2传递当前歌曲信息给 API
JsonNode recommendations = apiService.getRecommendations(userId, currentSong);
// 修正 3解析 API 返回的实际内容
JsonNode contentNode = recommendations.get("choices").get(0).get("message").get("content");
List<Song> recommendedSongs = parseRecommendations(contentNode);
// 更新缓存
updateCache(cacheKey, recommendedSongs);
return recommendedSongs;
} catch (Exception e) {
logger.error("获取推荐失败", e);
return getFallbackRecommendations(currentSong);
}
}
/**
*
*/
private Map<String, Object> buildUserPreferences(String userId, Song currentSong) {
Map<String, Object> preferences = new HashMap<>();
preferences.put("userId", userId);
preferences.put("currentSong", currentSong.getTitle());
preferences.put("artist", currentSong.getArtist());
preferences.put("genre", currentSong.getGenre());
preferences.put("timestamp", System.currentTimeMillis());
return preferences;
}
/**
*
*/
private List<Song> parseRecommendations(JsonNode contentNode) {
List<Song> songs = new ArrayList<>();
try {
// 假设 API 返回的是 JSON 数组字符串
String jsonArray = contentNode.asText();
JsonNode arrayNode = new ObjectMapper().readTree(jsonArray);
for (JsonNode node : arrayNode) {
Song song = new Song(
node.get("title").asText(),
node.get("artist").asText(),
"", // 专辑可能不存在
"", // 路径需要后续处理
0 // 时长
);
if (node.has("genre")) {
song.setGenre(node.get("genre").asText());
}
songs.add(song);
}
} catch (Exception e) {
logger.error("解析推荐失败", e);
}
return songs;
}
/**
* API
*/
private List<Song> getFallbackRecommendations(Song currentSong) {
// 这里可以实现一个简单的基于当前歌曲的推荐逻辑
// 例如:返回相同艺术家的其他歌曲
return new ArrayList<>();
}
/**
*
*/
private boolean isCacheValid(String userId) {
if (!userRecommendations.containsKey(userId) || !lastUpdateTime.containsKey(userId)) {
return false;
}
long currentTime = System.currentTimeMillis();
return (currentTime - lastUpdateTime.get(userId)) < CACHE_DURATION;
}
/**
*
*/
private void updateCache(String userId, List<Song> recommendations) {
userRecommendations.put(userId, recommendations);
lastUpdateTime.put(userId, System.currentTimeMillis());
}
/**
*
*/
public void clearUserCache(String userId, Song currentSong) {
String cacheKey = userId + "-" + currentSong.getId();
userRecommendations.remove(cacheKey);
lastUpdateTime.remove(cacheKey);
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.services;
public class SearchService {
}

@ -1,4 +0,0 @@
package com.musicPlayer.services;
public class SettingsService {
}

@ -1,4 +0,0 @@
package com.musicPlayer.services;
public class TimerService {
}

@ -1,55 +0,0 @@
package com.musicPlayer.services;
import com.musicPlayer.model.User;
import java.sql.*;
public class UserService {
private static final String DB_URL = "jdbc:sqlite:musicplayer.db";
public UserService() {
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL)");
} catch (SQLException e) {
e.printStackTrace();
}
}
public boolean register(String username, String password) {
String sql = "INSERT INTO user (username, password) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.setString(2, password);
pstmt.executeUpdate();
return true;
} catch (SQLException e) {
return false;
}
}
public boolean login(String username, String password) {
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
try (Connection conn = DriverManager.getConnection(DB_URL);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
return rs.next();
} catch (SQLException e) {
return false;
}
}
public boolean userExists(String username) {
String sql = "SELECT * FROM user WHERE username = ?";
try (Connection conn = DriverManager.getConnection(DB_URL);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
return rs.next();
} catch (SQLException e) {
return false;
}
}
}

@ -1,23 +0,0 @@
package com.musicPlayer.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class ConfigUtil {
private static final Properties properties = new Properties();
static {
try (InputStream input = ConfigUtil.class.getClassLoader().getResourceAsStream("config.properties")) {
if (input != null) {
properties.load(input);
}
} catch (IOException e) {
System.err.println("无法加载配置文件: " + e.getMessage());
}
}
public static String get(String key) {
return properties.getProperty(key);
}
}

@ -1,240 +0,0 @@
package com.musicPlayer.util;
import java.io.File;
import java.io.IOException; // Required for StringReader
import java.io.StringReader; // Required for parsing lyrics from DB
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger; // Added for logging
import org.slf4j.LoggerFactory; // Added for logging
import com.musicPlayer.model.LyricLine; // Required for convertLyricLinesToString
import com.musicPlayer.model.Song;
public class DBUtil {
private static final Logger logger = LoggerFactory.getLogger(DBUtil.class); // Added logger
private static final String DB_URL = "jdbc:sqlite:musicplayer.db";
private static final String INIT_SCRIPT_PATH = "src/main/resources/db/init.sql"; // Make sure this path is correct
public static Connection getConnection() throws SQLException {
// No need to call initializeDatabase() on every connection request.
// It should be called once at application startup or when DBUtil is first instantiated.
// For simplicity here, we'll keep it, but in a larger app, manage initialization separately.
initializeDatabase();
return DriverManager.getConnection(DB_URL);
}
private static synchronized void initializeDatabase() { // Added synchronized
File dbFile = new File("musicplayer.db");
if (!dbFile.exists()) {
logger.info("Database file not found. Initializing database...");
try {
// Ensure the resource path is correct, especially if running from a JAR vs IDE
String sqlScriptPath = INIT_SCRIPT_PATH;
java.net.URL scriptUrl = DBUtil.class.getClassLoader().getResource("db/init.sql");
String sql;
if (scriptUrl != null) {
sql = new String(Files.readAllBytes(Paths.get(scriptUrl.toURI())), StandardCharsets.UTF_8);
} else if (Files.exists(Paths.get(sqlScriptPath))) { // Fallback for direct file system access (IDE context)
sql = new String(Files.readAllBytes(Paths.get(sqlScriptPath)), StandardCharsets.UTF_8);
} else {
logger.error("init.sql script not found at {} or via ClassLoader.", sqlScriptPath);
throw new IOException("Database initialization script not found.");
}
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
// SQLite JDBC driver creates the file if it doesn't exist on first connection.
// The executeUpdate will then run the script.
String[] statements = sql.split(";"); // Split script into individual statements
for (String s : statements) {
if (!s.trim().isEmpty()) {
stmt.executeUpdate(s);
}
}
logger.info("✅ 数据库初始化完成 (Database initialized successfully)");
}
} catch (Exception e) {
logger.error("❌ 数据库初始化失败 (Database initialization failed): {}", e.getMessage(), e);
}
} else {
// logger.debug("Database file already exists."); // Optional: log if DB already exists
}
}
// Helper method to convert List<LyricLine> to a single String for DB storage
private String convertLyricLinesToString(List<LyricLine> lyricLines) {
if (lyricLines == null || lyricLines.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (LyricLine line : lyricLines) {
long totalMillis = (long) line.getTimestamp().toMillis();
long minutes = totalMillis / 60000;
long seconds = (totalMillis % 60000) / 1000;
long millis = totalMillis % 1000;
// Format: [mm:ss.xx] for centiseconds or [mm:ss.xxx] for milliseconds.
// LrcParser handles both [mm:ss.xx] and [mm:ss.xxx].
// Let's use centiseconds (2 digits for fractional part) as it's common.
String timeStr = String.format("[%02d:%02d.%02d]", minutes, seconds, millis / 10);
sb.append(timeStr).append(line.getText()).append("\n");
}
return sb.toString();
}
public void saveSong(Song song) {
String lyricsForDb = convertLyricLinesToString(song.getLyrics());
String sql = "INSERT INTO songs(title, artist, album, file_path, duration, album_art_data, lyrics_content) VALUES(?,?,?,?,?,?,?)";
try (Connection conn = getConnection(); // Ensure DB is initialized
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, song.getTitle());
pstmt.setString(2, song.getArtist());
pstmt.setString(3, song.getAlbum());
pstmt.setString(4, song.getFilePath());
pstmt.setInt(5, song.getDuration());
pstmt.setBytes(6, song.getAlbumArtData()); // Save album art
pstmt.setString(7, lyricsForDb); // Save lyrics content
pstmt.executeUpdate();
try (ResultSet rs = pstmt.getGeneratedKeys()) {
if (rs.next()) {
song.setId(rs.getInt(1));
}
}
} catch (SQLException e) {
logger.error("Error saving song to DB: {}", song.getTitle(), e);
}
}
public void updateSong(Song song) {
String lyricsForDb = convertLyricLinesToString(song.getLyrics());
String sql = "UPDATE songs SET title = ?, artist = ?, album = ?, duration = ?, album_art_data = ?, lyrics_content = ? WHERE id = ?";
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, song.getTitle());
pstmt.setString(2, song.getArtist());
pstmt.setString(3, song.getAlbum());
pstmt.setInt(4, song.getDuration());
pstmt.setBytes(5, song.getAlbumArtData());
pstmt.setString(6, lyricsForDb);
pstmt.setInt(7, song.getId());
pstmt.executeUpdate();
logger.info("Updated song in DB: {}", song.getTitle());
} catch (SQLException e) {
logger.error("Error updating song in DB: {}", song.getTitle(), e);
}
}
public List<Song> getAllSongs() {
List<Song> songs = new ArrayList<>();
String sql = "SELECT * FROM songs ORDER BY title";
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Song song = new Song(
rs.getString("title"),
rs.getString("artist"),
rs.getString("album"),
rs.getString("file_path"),
rs.getInt("duration")
);
song.setId(rs.getInt("id"));
song.setAlbumArtData(rs.getBytes("album_art_data")); // Load album art
String lyricsContentFromDb = rs.getString("lyrics_content");
if (lyricsContentFromDb != null && !lyricsContentFromDb.isEmpty()) {
try (StringReader sr = new StringReader(lyricsContentFromDb)) {
song.setLyrics(LrcParser.parse(sr)); // Parse lyrics
} catch (IOException ioe) {
logger.error("Failed to parse lyrics from DB for song: {}", song.getTitle(), ioe);
song.setLyrics(new ArrayList<>()); // Default to empty list on parsing error
}
} else {
song.setLyrics(new ArrayList<>()); // Default to empty list if no lyrics in DB
}
songs.add(song);
}
} catch (SQLException e) {
logger.error("Error retrieving all songs from DB", e);
}
return songs;
}
public Song findSongByPath(String filePath) {
String sql = "SELECT * FROM songs WHERE file_path = ?";
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, filePath);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
Song song = new Song(
rs.getString("title"),
rs.getString("artist"),
rs.getString("album"),
rs.getString("file_path"),
rs.getInt("duration")
);
song.setId(rs.getInt("id"));
song.setAlbumArtData(rs.getBytes("album_art_data")); // Load album art
String lyricsContentFromDb = rs.getString("lyrics_content");
if (lyricsContentFromDb != null && !lyricsContentFromDb.isEmpty()) {
try (StringReader sr = new StringReader(lyricsContentFromDb)) {
song.setLyrics(LrcParser.parse(sr)); // Parse lyrics
} catch (IOException ioe) {
logger.error("Failed to parse lyrics from DB for song (by path): {}", song.getTitle(), ioe);
song.setLyrics(new ArrayList<>());
}
} else {
song.setLyrics(new ArrayList<>());
}
return song;
}
}
} catch (SQLException e) {
logger.error("Error finding song by path: {}", filePath, e);
}
return null;
}
public void deleteSong(Song song) {
if (song == null) return;
// Can use ID if available and more reliable, otherwise filePath
String sql = (song.getId() > 0) ? "DELETE FROM songs WHERE id = ?" : "DELETE FROM songs WHERE file_path = ?";
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
if (song.getId() > 0) {
pstmt.setInt(1, song.getId());
} else {
pstmt.setString(1, song.getFilePath());
}
pstmt.executeUpdate();
logger.info("Deleted song: {}", song.getTitle());
} catch (SQLException e) {
logger.error("Error deleting song: {}", song.getTitle(), e);
}
}
// Removed static from addSong, songExists for consistency if DBUtil is instanced.
// If DBUtil is purely static methods, then keep them static and ensure getConnection handles init.
// For now, assuming it might be instanced by MusicPlayerService or used statically as is.
// The key changes are in saveSong, getAllSongs, findSongByPath.
}

@ -1,187 +0,0 @@
package com.musicPlayer.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.musicPlayer.model.Song;
public class FFmpegUtil {
private static final String FFMPEG_EXECUTABLE =
System.getProperty("os.name").toLowerCase().contains("win") ? "ffmpeg.exe" : "ffmpeg";
public static String configureFromEnv() {
if (testFFmpeg(FFMPEG_EXECUTABLE)) {
return FFMPEG_EXECUTABLE;
}
String pathEnv = System.getenv("PATH");
if (pathEnv != null) {
String[] paths = pathEnv.split(System.getProperty("path.separator"));
for (String path : paths) {
Path ffmpegPath = Paths.get(path, FFMPEG_EXECUTABLE);
if (Files.isExecutable(ffmpegPath)) {
return ffmpegPath.toString();
}
}
}
return null;
}
public static boolean testFFmpeg(String ffmpegCommand) {
try {
Process process = new ProcessBuilder(ffmpegCommand, "-version").start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String firstLine = reader.readLine();
return firstLine != null
&& firstLine.contains("ffmpeg")
&& process.waitFor() == 0;
}
} catch (IOException | InterruptedException e) {
return false;
}
}
public static String getConfigStatus() {
String path = configureFromEnv();
if (path != null) {
return "✅ FFmpeg配置成功 (路径: " + path + ")";
} else {
return "❌ FFmpeg未找到请确保已添加到PATH环境变量中";
}
}
public static Song extractMetadata(File file) {
// 基础文件名作为title的fallback
String fallbackTitle = getBaseNameWithoutExtension(file.getName());
try {
// 构建FFprobe命令使用完整JSON输出
Process process = new ProcessBuilder(
"ffprobe",
"-v", "error", // 只显示错误
"-show_entries",
"format=duration:format_tags=title,artist,album",
"-print_format", "json",
"-i", file.getAbsolutePath()
).redirectErrorStream(true).start();
// 读取完整JSON输出使用UTF-8编码
String jsonOutput;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
jsonOutput = reader.lines().collect(Collectors.joining());
}
// 等待进程结束
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("FFprobe执行失败退出码: " + exitCode);
}
// 使用Jackson解析JSON
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonOutput);
JsonNode formatNode = rootNode.path("format");
JsonNode tagsNode = formatNode.path("tags");
// 提取元数据(处理可能的缺失字段)
String title = tagsNode.path("title").asText(fallbackTitle);
String artist = tagsNode.path("artist").asText("未知艺术家");
String album = tagsNode.path("album").asText(null);
// 处理duration秒转为毫秒
double durationSeconds = formatNode.path("duration").asDouble(0);
int durationMs = (int) (durationSeconds * 1000);
return new Song(
truncateUtf8(title, 100), // 限制长度防止数据库截断
truncateUtf8(artist, 50),
album != null ? truncateUtf8(album, 50) : null,
file.getAbsolutePath(),
durationMs
);
} catch (Exception e) {
System.err.printf("解析文件元数据失败: %s (%s)%n",
file.getAbsolutePath(), e.getMessage());
return new Song(
fallbackTitle,
"未知艺术家",
null,
file.getAbsolutePath(),
0
);
}
}
// 正确处理UTF-8字符串截断
private static String truncateUtf8(String str, int maxBytes) {
if (str == null || str.isEmpty()) return str;
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
if (bytes.length <= maxBytes) return str;
// 找到不超过maxBytes的最后一个完整字符
int end = maxBytes;
while (end > 0 && (bytes[end] & 0xC0) == 0x80) {
end--;
}
return new String(bytes, 0, end, StandardCharsets.UTF_8) + (end < bytes.length ? "..." : "");
}
// 获取无扩展名的文件名
private static String getBaseNameWithoutExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
}
private static String getFileExtension(File file) {
String name = file.getName();
int lastIndexOf = name.lastIndexOf(".");
if (lastIndexOf == -1) {
return ""; // 没有扩展名
}
return name.substring(lastIndexOf);
}
private static String extractFromJson(String json, String key) {
String pattern = "\"" + key + "\":\"(.*?)\"";
java.util.regex.Pattern r = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher m = r.matcher(json);
if (m.find()) {
return m.group(1);
}
return null;
}
// 新增转码为mp3临时文件
public static File transcodeToMp3(File inputFile) throws IOException, InterruptedException {
String outputPath = inputFile.getAbsolutePath() + ".temp.mp3";
ProcessBuilder pb = new ProcessBuilder(
FFMPEG_EXECUTABLE, "-y", "-i", inputFile.getAbsolutePath(), outputPath
);
pb.redirectErrorStream(true);
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[FFmpeg] " + line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) throw new IOException("FFmpeg转码失败退出码: " + exitCode);
return new File(outputPath);
}
}

@ -1,87 +0,0 @@
package com.musicPlayer.util;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
public class HttpUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
private static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* GET
* @param url URL
* @param headers
* @return
*/
public static String get(String url, Map<String, String> headers) throws IOException, InterruptedException {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET();
if (headers != null) {
headers.forEach(requestBuilder::header);
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP请求失败状态码: " + response.statusCode());
}
return response.body();
}
/**
* POST
* @param url URL
* @param headers
* @param body
* @return
*/
public static String post(String url, Map<String, String> headers, String body) throws IOException, InterruptedException {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(body));
if (headers != null) {
headers.forEach(requestBuilder::header);
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP请求失败状态码: " + response.statusCode());
}
return response.body();
}
/**
* JSON
*/
public static String toJson(Object obj) throws IOException {
return objectMapper.writeValueAsString(obj);
}
/**
* JSON
*/
public static <T> T fromJson(String json, Class<T> clazz) throws IOException {
return objectMapper.readValue(json, clazz);
}
}

@ -1,120 +0,0 @@
package com.musicPlayer.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader; // Added
import java.io.StringReader; // Added for convenience if directly used
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import com.musicPlayer.model.LyricLine;
import javafx.util.Duration;
public class LrcParser {
private static final Pattern timestampPattern = Pattern.compile("\\[(\\d{2}):(\\d{2})[.:](\\d{2,3})\\]");
public static List<LyricLine> parse(File lrcFile) throws IOException {
List<LyricLine> lyrics = new ArrayList<>();
if (!lrcFile.exists() || !lrcFile.isFile()) {
// System.out.println("LRC file does not exist or is not a file: " + lrcFile.getAbsolutePath());
return lyrics; // File doesn't exist or is not a file, return empty list
}
try (BufferedReader reader = new BufferedReader(new FileReader(lrcFile))) {
return parse(reader); // Delegate to the Reader-based method
}
}
// Overloaded method to parse from any Reader
public static List<LyricLine> parse(Reader inputReader) throws IOException {
List<LyricLine> lyrics = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(inputReader)) {
String line;
while ((line = reader.readLine()) != null) {
parseLine(line, lyrics);
}
}
// Sort lyrics by timestamp after parsing all lines
Collections.sort(lyrics, Comparator.comparing(LyricLine::getTimestamp));
return lyrics;
}
private static void parseLine(String line, List<LyricLine> lyrics) {
Matcher matcher = timestampPattern.matcher(line);
List<Duration> timestamps = new ArrayList<>();
int lastMatchEnd = 0;
// Extract all timestamps from the beginning of the line
while (matcher.find() && matcher.start() == lastMatchEnd) {
int minutes = Integer.parseInt(matcher.group(1));
int seconds = Integer.parseInt(matcher.group(2));
String millisOrCentis = matcher.group(3);
int milliseconds = 0;
if (millisOrCentis.length() == 3) { // Milliseconds
milliseconds = Integer.parseInt(millisOrCentis);
} else if (millisOrCentis.length() == 2) { // Centiseconds
milliseconds = Integer.parseInt(millisOrCentis) * 10;
}
timestamps.add(Duration.seconds(minutes * 60 + seconds).add(Duration.millis(milliseconds)));
lastMatchEnd = matcher.end();
}
// The text part of the lyric is after all timestamps
String text = line.substring(lastMatchEnd).trim();
// If timestamps were found, create LyricLine objects
if (!timestamps.isEmpty()) {
for (Duration timestamp : timestamps) {
lyrics.add(new LyricLine(timestamp, text));
}
} else if (!line.trim().isEmpty() && !timestampPattern.matcher(line.trim()).find() && !isMetadataTag(line)) {
// Handle lines that are purely text (no timestamps) - potentially add as untimed or ignore
// For now, we only add lines that have associated timestamps as per typical LRC format
// System.out.println("Skipping non-timestamped, non-metadata line: " + line);
}
}
private static boolean isMetadataTag(String line) {
// Simple check for common LRC metadata tags like [ti:], [ar:], [al:]
return line.matches("^\\[(ti|ar|al|au|by|offset|re|ve|length):.*\\]$");
}
public static File autoGenerateLrcWithTimestamps(File mp3File, File rawLyricFile, int intervalSec) throws IOException {
List<String> lines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(rawLyricFile), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty()) {
lines.add(line.trim());
}
}
}
String mp3Path = mp3File.getAbsolutePath();
String lrcPath = mp3Path.substring(0, mp3Path.lastIndexOf('.')) + ".lrc";
File lrcFile = new File(lrcPath);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(lrcFile), "UTF-8"))) {
for (int i = 0; i < lines.size(); i++) {
int totalSec = i * intervalSec;
int min = totalSec / 60;
int sec = totalSec % 60;
writer.write(String.format("[%02d:%02d.00]%s", min, sec, lines.get(i)));
writer.newLine();
}
}
return lrcFile;
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class LoginViewController {
}

@ -1,5 +0,0 @@
package com.musicPlayer.viewControllers;
public class MusicPlayerViewController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class PlaylistViewController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class SearchViewController {
}

@ -1,359 +0,0 @@
package com.musicPlayer.viewControllers;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import java.net.URL;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;
public class SettingsViewController implements Initializable {
// === UI Components ===
@FXML private TableView<Shortcut> shortcutsTable;
@FXML private TableColumn<Shortcut, String> functionColumn;
@FXML private TableColumn<Shortcut, String> shortcutColumn;
@FXML private CheckBox enableShortcutsCheckBox;
@FXML private TextField shortcutInput;
@FXML private Button confirmButton;
@FXML private Button cancelButton;
@FXML private Button applyButton;
@FXML private Button restoreDefaultsButton;
private Shortcut currentlyEditing;
// === Data Model ===
private final ObservableList<Shortcut> shortcutData = FXCollections.observableArrayList(
new Shortcut("播放/暂停", "Ctrl+Shift+F5"),
new Shortcut("停止", "Ctrl+Shift+F6"),
new Shortcut("快进", "Ctrl+Shift+F8"),
new Shortcut("快退", "Ctrl+Shift+F7"),
new Shortcut("上一曲", "Ctrl+Shift+Left"),
new Shortcut("下一曲", "Ctrl+Shift+Right"),
new Shortcut("增大音量", "Ctrl+Shift+Up"),
new Shortcut("减小音量", "Ctrl+Shift+Down"),
new Shortcut("退出", ""),
new Shortcut("显示/隐藏播放器", ""),
new Shortcut("显示/隐藏桌面歌词", ""),
new Shortcut("添加到我喜欢的音乐", "")
);
// === Initialization ===
@Override
public void initialize(URL location, ResourceBundle resources) {
setupTableColumns();
setupEnableCheckbox();
loadSavedShortcuts();
setupButtonActions();
// 设置行选择监听
shortcutsTable.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null) {
currentlyEditing = newVal;
shortcutInput.setText(newVal.getShortcut().isEmpty() ? "" : newVal.getShortcut());
}
});
// 输入框按键监听
shortcutInput.setOnKeyPressed(this::captureShortcut);
}
private void setupTableColumns() {
functionColumn.setCellValueFactory(new PropertyValueFactory<>("function"));
shortcutColumn.setCellValueFactory(new PropertyValueFactory<>("formattedShortcut"));
shortcutColumn.setCellFactory(param -> new ShortcutCell());
// 设置列宽比例
functionColumn.setPrefWidth(380);
shortcutColumn.setPrefWidth(300);
}
private void setupEnableCheckbox() {
enableShortcutsCheckBox.selectedProperty().addListener((obs, oldVal, enabled) -> {
shortcutsTable.setDisable(!enabled);
if (!enabled) {
shortcutsTable.getSelectionModel().clearSelection();
}
});
}
private void loadSavedShortcuts() {
Preferences prefs = Preferences.userNodeForPackage(MusicPlayerViewController.class);
// 加载快捷键
shortcutData.forEach(shortcut -> {
String savedShortcut = prefs.get("shortcut." + shortcut.getFunction(), shortcut.getShortcut());
shortcut.setShortcut(savedShortcut);
});
// 加载启用状态
enableShortcutsCheckBox.setSelected(prefs.getBoolean("shortcuts.enabled", true));
shortcutsTable.setItems(shortcutData);
}
private void setupButtonActions() {
confirmButton.setOnAction(e -> handleConfirm());
cancelButton.setOnAction(e -> handleCancel());
applyButton.setOnAction(e -> handleApply());
restoreDefaultsButton.setOnAction(e -> handleRestoreDefaults());
}
// === Shortcut Capture ===
private void captureShortcut(KeyEvent event) {
String newShortcut = buildShortcutString(event);
shortcutInput.setText(newShortcut);
event.consume();
}
private String buildShortcutString(KeyEvent event) {
StringBuilder sb = new StringBuilder();
if (event.isControlDown()) sb.append("Ctrl+");
if (event.isShiftDown()) sb.append("Shift+");
if (event.isAltDown()) sb.append("Alt+");
if (!event.getCode().isModifierKey()) {
sb.append(event.getCode().getName());
} else if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
@FXML
public void handleShortcutUpdate(ActionEvent event) {
if (currentlyEditing != null && !shortcutInput.getText().isEmpty()) {
// 检查快捷键冲突
if (isShortcutConflict(shortcutInput.getText(), currentlyEditing)) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("快捷键冲突");
alert.setHeaderText("该快捷键已被其他功能使用");
alert.setContentText("请选择其他快捷键组合");
alert.showAndWait();
return;
}
currentlyEditing.setShortcut(shortcutInput.getText());
shortcutsTable.refresh();
}
}
private boolean isShortcutConflict(String newShortcut, Shortcut currentShortcut) {
if (newShortcut.isEmpty()) return false;
return shortcutData.stream()
.filter(s -> !s.equals(currentShortcut))
.anyMatch(s -> s.getShortcut().equals(newShortcut));
}
// === Button Handlers ===
@FXML
private void handleConfirm() {
saveShortcuts();
closeWindow();
}
@FXML
private void handleCancel() {
closeWindow();
}
@FXML
private void handleApply() {
saveShortcuts();
}
@FXML
private void handleRestoreDefaults() {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("恢复默认设置");
alert.setHeaderText("确定要恢复默认快捷键吗?");
alert.setContentText("所有自定义快捷键设置将被重置");
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
ObservableList<Shortcut> defaultData = FXCollections.observableArrayList(
new Shortcut("播放/暂停", "Ctrl+Shift+F5"),
new Shortcut("停止", "Ctrl+Shift+F6"),
new Shortcut("快进", "Ctrl+Shift+F8"),
new Shortcut("快退", "Ctrl+Shift+F7"),
new Shortcut("上一曲", "Ctrl+Shift+Left"),
new Shortcut("下一曲", "Ctrl+Shift+Right"),
new Shortcut("增大音量", "Ctrl+Shift+Up"),
new Shortcut("减小音量", "Ctrl+Shift+Down"),
new Shortcut("退出", ""),
new Shortcut("显示/隐藏播放器", ""),
new Shortcut("显示/隐藏桌面歌词", ""),
new Shortcut("添加到我喜欢的音乐", "")
);
shortcutData.setAll(defaultData);
shortcutsTable.refresh();
enableShortcutsCheckBox.setSelected(true);
}
}
// === Utility Methods ===
private void saveShortcuts() {
Preferences prefs = Preferences.userNodeForPackage(MusicPlayerViewController.class);
// 保存所有快捷键
shortcutData.forEach(shortcut ->
prefs.put("shortcut." + shortcut.getFunction(), shortcut.getShortcut())
);
// 保存启用状态
prefs.putBoolean("shortcuts.enabled", enableShortcutsCheckBox.isSelected());
// 显示保存成功提示
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("保存成功");
alert.setHeaderText(null);
alert.setContentText("快捷键设置已保存");
alert.showAndWait();
}
private void closeWindow() {
Stage stage = (Stage) shortcutsTable.getScene().getWindow();
stage.close();
}
// === Custom Table Cell ===
private class ShortcutCell extends TableCell<Shortcut, String> {
private final Button setButton = new Button("设置");
private final Label shortcutLabel = new Label();
public ShortcutCell() {
setButton.setStyle("-fx-background-color: #4a90e2; -fx-text-fill: white;");
setButton.setOnAction(e -> startKeyBinding());
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTableRow() == null) {
setGraphic(null);
return;
}
Shortcut shortcut = getTableRow().getItem();
if (shortcut == null) return;
// 特殊处理"添加到我喜欢的音乐"行
if ("添加到我喜欢的音乐".equals(shortcut.getFunction())) {
HBox container = new HBox(0, new Label(""), setButton);
container.setAlignment(Pos.CENTER_LEFT);
setGraphic(container);
getTableRow().setStyle("-fx-background-color: #e6f2ff;");
}
// 处理其他行的显示
else if (shortcut.getShortcut().isEmpty()) {
setGraphic(setButton);
} else {
shortcutLabel.setText(shortcut.getFormattedShortcut());
setGraphic(shortcutLabel);
}
}
private void startKeyBinding() {
setGraphic(new Label("按下按键组合..."));
// 临时保存原始快捷键
Shortcut shortcut = getTableRow().getItem();
String originalShortcut = shortcut.getShortcut();
// 设置按键监听
getScene().setOnKeyPressed(event -> {
String newShortcut = buildShortcutString(event);
// 检查快捷键冲突
if (isShortcutConflict(newShortcut, shortcut)) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("快捷键冲突");
alert.setHeaderText("该快捷键已被其他功能使用");
alert.setContentText("请选择其他快捷键组合");
alert.showAndWait();
return;
}
// 更新数据
shortcut.setShortcut(newShortcut);
updateItem(shortcut.getFormattedShortcut(), false);
// 移除监听
getScene().setOnKeyPressed(null);
});
// 点击空白处取消
getScene().setOnMouseClicked(event -> {
shortcut.setShortcut(originalShortcut);
updateItem(shortcut.getFormattedShortcut(), false);
getScene().setOnKeyPressed(null);
});
}
private boolean isShortcutConflict(String newShortcut, Shortcut currentShortcut) {
if (newShortcut.isEmpty()) return false;
return shortcutData.stream()
.filter(s -> !s.equals(currentShortcut))
.anyMatch(s -> s.getShortcut().equals(newShortcut));
}
private String buildShortcutString(KeyEvent event) {
StringBuilder sb = new StringBuilder();
if (event.isControlDown()) sb.append("Ctrl+");
if (event.isShiftDown()) sb.append("Shift+");
if (event.isAltDown()) sb.append("Alt+");
if (!event.getCode().isModifierKey()) {
sb.append(event.getCode().getName());
} else if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
}
// === Data Model Class ===
public static class Shortcut {
private final StringProperty function;
private final StringProperty shortcut;
private final StringProperty formattedShortcut;
public Shortcut(String function, String shortcut) {
this.function = new SimpleStringProperty(function);
this.shortcut = new SimpleStringProperty(shortcut);
this.formattedShortcut = new SimpleStringProperty(
shortcut.isEmpty() ? "" : shortcut.replace("+", " + ")
);
}
public void setShortcut(String newShortcut) {
shortcut.set(newShortcut);
formattedShortcut.set(
newShortcut.isEmpty() ? "" : newShortcut.replace("+", " + ")
);
}
// Getters
public String getFunction() { return function.get(); }
public String getShortcut() { return shortcut.get(); }
public String getFormattedShortcut() { return formattedShortcut.get(); }
}
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class SocialViewController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class TimerViewController {
}

@ -1,4 +0,0 @@
package com.musicPlayer.viewControllers;
public class UserViewController {
}

@ -1,19 +0,0 @@
<VBox spacing="10" styleClass="main-container">
<!-- 现有的播放列表 -->
<ListView fx:id="songListView" VBox.vgrow="ALWAYS" styleClass="song-list">
<cellFactory>
<SongCellFactory />
</cellFactory>
</ListView>
<!-- 添加推荐列表 -->
<Label text="推荐歌曲" styleClass="section-label"/>
<ListView fx:id="recommendationListView" VBox.vgrow="ALWAYS" styleClass="recommendation-list">
<cellFactory>
<SongCellFactory />
</cellFactory>
</ListView>
<!-- 现有的控制按钮 -->
// ... existing controls ...
</VBox>

@ -1,27 +0,0 @@
.recommendation-list {
-fx-background-color: #2a2a2a;
-fx-background-insets: 0;
-fx-padding: 5;
}
.recommendation-list .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #ffffff;
-fx-padding: 5 10;
}
.recommendation-list .list-cell:selected {
-fx-background-color: #1DB954;
-fx-text-fill: #ffffff;
}
.recommendation-list .list-cell:hover {
-fx-background-color: #404040;
}
.section-label {
-fx-text-fill: #ffffff;
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-padding: 5 10;
}

@ -1 +0,0 @@
deepseek.api.key=sk-5e865615b5d1450ea412d6a9efe961f9

@ -1,31 +0,0 @@
-- 歌曲表
CREATE TABLE IF NOT EXISTS songs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
artist TEXT,
album TEXT,
file_path TEXT UNIQUE NOT NULL,
duration INTEGER,
album_art_data BLOB, -- Added for album art
lyrics_content TEXT -- Added for raw lyrics text
);
-- 播放列表表
CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- 播放列表-歌曲关联表
CREATE TABLE IF NOT EXISTS playlist_songs (
playlist_id INTEGER,
song_id INTEGER,
position INTEGER,
PRIMARY KEY (playlist_id, song_id),
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE
);
-- 创建索引提升查询性能
CREATE INDEX IF NOT EXISTS idx_song_artist ON songs(artist);
CREATE INDEX IF NOT EXISTS idx_playlist_name ON playlists(name);

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox alignment="CENTER" spacing="22" xmlns="http://javafx.com/javafx/21.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.musicPlayer.controller.LoginController" stylesheets="@../styles/main.css">
<Label text="用户登录" styleClass="song-title"/>
<TextField fx:id="usernameField" promptText="用户名" maxWidth="260" styleClass="text-field"/>
<PasswordField fx:id="passwordField" promptText="密码" maxWidth="260" styleClass="password-field"/>
<Button fx:id="loginBtn" text="登录" minWidth="260" styleClass="button" onAction="#handleLogin"/>
<Hyperlink fx:id="toRegisterLink" text="没有账号?去注册" onAction="#handleToRegister" styleClass="hyperlink"/>
</VBox>

@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<?import javafx.geometry.Insets?>
<BorderPane xmlns="http://javafx.com/javafx/21.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.musicPlayer.controller.MusicPlayerController"
stylesheets="@../styles/main.css">
<top>
<MenuBar BorderPane.alignment="CENTER">
<menus>
<Menu text="文件">
<items>
<MenuItem onAction="#handleAddSongs" text="添加歌曲" />
<MenuItem onAction="#handleExit" text="退出" />
</items>
</Menu>
<Menu text="设置">
<items>
<MenuItem onAction="#handleSettings" text="打开设置" />
</items>
</Menu>
</menus>
</MenuBar>
</top>
<center>
<SplitPane dividerPositions="0.3" styleClass="main-split-pane">
<items>
<!-- 左侧歌曲列表 -->
<VBox spacing="10" styleClass="playlist-panel">
<padding>
<Insets top="10" right="10" bottom="10" left="10"/>
</padding>
<Label text="歌曲列表" styleClass="playlist-title"/>
<ListView fx:id="songListView" VBox.vgrow="ALWAYS"/>
</VBox>
<!-- 右侧主内容区 -->
<VBox spacing="10">
<!-- 专辑封面和歌曲信息 -->
<HBox spacing="20" alignment="CENTER" styleClass="info-panel" VBox.vgrow="ALWAYS">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
<!-- 使用StackPane来叠放图片和可能的其他元素方便以后扩展 -->
<StackPane HBox.hgrow="ALWAYS" alignment="CENTER">
<ImageView fx:id="albumCoverView" fitWidth="200" fitHeight="200" styleClass="album-cover" preserveRatio="true"/>
<!-- TODO: 可以在这里添加占位符文字或图标 -->
</StackPane>
<VBox spacing="10" alignment="TOP_LEFT" HBox.hgrow="ALWAYS">
<Label fx:id="currentSongLabel" text="No song selected" styleClass="song-title" wrapText="true"/>
<Label fx:id="artistLabel" text="未知艺术家" styleClass="artist-label" wrapText="true"/>
<Label fx:id="albumLabel" text="未知专辑" styleClass="album-label" wrapText="true"/>
<!-- TODO: 可以在这里添加更多歌曲信息 -->
</VBox>
</HBox>
<!-- 歌词显示区域 -->
<ListView fx:id="lyricsView" VBox.vgrow="ALWAYS" styleClass="lyrics-view"/>
<!-- 播放控制区域 -->
<VBox spacing="10" styleClass="control-panel">
<!-- 进度条 -->
<HBox spacing="10" alignment="CENTER">
<Label fx:id="timeLabel" text="00:00 / 00:00" minWidth="100"/>
<Slider fx:id="progressSlider" HBox.hgrow="ALWAYS"/>
</HBox>
<!-- 控制按钮和音量 -->
<HBox spacing="20" alignment="CENTER">
<Region HBox.hgrow="ALWAYS"/> <!-- 左侧占位符 -->
<Button fx:id="previousButton" text="⏮" onAction="#handlePreviousSong" styleClass="control-button"/>
<Button fx:id="playPauseButton" text="▶" onAction="#handlePlayPause" styleClass="control-button"/>
<Button fx:id="nextButton" text="⏭" onAction="#handleNextSong" styleClass="control-button"/>
<Button fx:id="generateRecommendBtn" text="生成推荐请求" onAction="#handleGenerateRecommendRequest" styleClass="button"/>
<Region HBox.hgrow="ALWAYS"/> <!-- 中间占位符 -->
<Label text="音量"/>
<Slider fx:id="volumeSlider" max="1" min="0" value="0.5" prefWidth="100"/>
<Region HBox.hgrow="ALWAYS"/> <!-- 右侧占位符 -->
</HBox>
</VBox>
</VBox>
</items>
</SplitPane>
</center>
</BorderPane>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox alignment="CENTER" spacing="15" xmlns="http://javafx.com/javafx/21.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.musicPlayer.controller.RegisterController" stylesheets="@../styles/main.css">
<Label text="用户注册" styleClass="song-title"/>
<TextField fx:id="usernameField" promptText="用户名" maxWidth="220" styleClass="text-field"/>
<PasswordField fx:id="passwordField" promptText="密码" maxWidth="220" styleClass="text-field"/>
<PasswordField fx:id="confirmPasswordField" promptText="确认密码" maxWidth="220" styleClass="text-field"/>
<Button fx:id="registerBtn" text="注册" minWidth="260" styleClass="button" onAction="#handleRegister"/>
<Hyperlink fx:id="toLoginLink" text="已有账号?去登录" onAction="#handleToLogin" styleClass="hyperlink"/>
</VBox>

@ -1,172 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.musicPlayer.viewControllers.SettingsViewController">
<children>
<TabPane layoutX="10.0" layoutY="10.0" prefHeight="600.0" prefWidth="800.0" tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab text="常规设置">
<content>
<AnchorPane>
<children>
<Label layoutX="14.0" layoutY="14.0" text="语言 (Language):" />
<ComboBox layoutX="153.0" layoutY="10.0" prefWidth="150.0" />
<CheckBox layoutX="14.0" layoutY="44.0" mnemonicParsing="false" text="开机自动运行" />
<CheckBox layoutX="14.0" layoutY="74.0" mnemonicParsing="false" text="启动时检查更新" />
<CheckBox layoutX="14.0" layoutY="104.0" mnemonicParsing="false" text="优先使用内嵌歌词" />
<CheckBox layoutX="14.0" layoutY="134.0" mnemonicParsing="false" text="显示歌词翻译(如果有)" />
<CheckBox layoutX="14.0" layoutY="164.0" mnemonicParsing="false" text="歌词卡拉OK样式显示" />
<CheckBox layoutX="14.0" layoutY="194.0" mnemonicParsing="false" text="歌词模糊匹配" />
<CheckBox layoutX="14.0" layoutY="224.0" mnemonicParsing="false" text="没有歌词时显示歌曲信息" />
<Button layoutX="14.0" layoutY="254.0" mnemonicParsing="false" text="浏览..." />
<Label layoutX="14.0" layoutY="284.0" text="歌词文件夹:" />
<TextField layoutX="153.0" layoutY="280.0" prefWidth="200.0" />
<Label layoutX="14.0" layoutY="314.0" text="歌词调整后:" />
<ComboBox layoutX="153.0" layoutY="310.0" prefWidth="150.0" />
<CheckBox layoutX="14.0" layoutY="344.0" mnemonicParsing="false" text="不显示歌词空行" />
</children>
</AnchorPane>
</content>
</Tab>
<Tab text="外观设置">
<content>
<AnchorPane>
<children>
<Label layoutX="14.0" layoutY="14.0" text="频谱分析高度:" />
<Slider layoutX="153.0" layoutY="10.0" prefWidth="200.0" />
<CheckBox layoutX="14.0" layoutY="44.0" mnemonicParsing="false" text="显示专辑封面" />
<CheckBox layoutX="14.0" layoutY="74.0" mnemonicParsing="false" text="深色模式" />
<CheckBox layoutX="14.0" layoutY="104.0" mnemonicParsing="false" text="启用背景" />
<CheckBox layoutX="14.0" layoutY="134.0" mnemonicParsing="false" text="使用专辑封面作为背景" />
<TextField layoutX="153.0" layoutY="164.0" prefWidth="200.0" />
<Label layoutX="14.0" layoutY="194.0" text="背景不透明度:" />
<Slider layoutX="153.0" layoutY="190.0" prefWidth="200.0" />
<Label layoutX="14.0" layoutY="224.0" text="高斯模糊半径:" />
<Slider layoutX="153.0" layoutY="220.0" prefWidth="200.0" />
<Label layoutX="14.0" layoutY="254.0" text="封面选项" />
<CheckBox layoutX="14.0" layoutY="284.0" mnemonicParsing="false" text="优先使用内嵌专辑封面" />
<CheckBox layoutX="14.0" layoutY="314.0" mnemonicParsing="false" text="没有专辑封面时使用外部图片" />
<TextField layoutX="153.0" layoutY="284.0" prefWidth="200.0" />
<Label layoutX="14.0" layoutY="344.0" text="主题颜色" />
<ColorPicker layoutX="153.0" layoutY="340.0" />
</children>
</AnchorPane>
</content>
</Tab>
<Tab text="播放设置">
<content>
<AnchorPane>
<children>
<CheckBox layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="出现错误时停止播放" />
<CheckBox layoutX="14.0" layoutY="44.0" mnemonicParsing="false" text="在任务栏显示播放进度" />
<CheckBox layoutX="14.0" layoutY="74.0" mnemonicParsing="false" text="声音淡入淡出效果" />
<CheckBox layoutX="14.0" layoutY="104.0" mnemonicParsing="false" text="使用系统媒体控件" />
<Label layoutX="14.0" layoutY="134.0" text="播放内核" />
<RadioButton layoutX="153.0" layoutY="130.0" mnemonicParsing="false" text="BASS" />
<RadioButton layoutX="250.0" layoutY="130.0" mnemonicParsing="false" text="FFMPEG" />
<Button layoutX="350.0" layoutY="130.0" mnemonicParsing="false" text="点击此处下载" />
<Label layoutX="14.0" layoutY="164.0" text="播放设备" />
<ComboBox layoutX="153.0" layoutY="160.0" prefWidth="150.0" />
<Label layoutX="14.0" layoutY="194.0" text="FFMPEG 核心设置" />
<Slider layoutX="153.0" layoutY="190.0" prefWidth="200.0" />
<Label layoutX="363.0" layoutY="194.0" text="缓冲时长 (秒):" />
<Slider layoutX="530.0" layoutY="190.0" prefWidth="200.0" />
<Label layoutX="363.0" layoutY="224.0" text="最大重试次数 (-1为无限制):" />
<Slider layoutX="530.0" layoutY="220.0" prefWidth="200.0" />
<Label layoutX="363.0" layoutY="254.0" text="非本地文件重试间隔 (秒):" />
<Slider layoutX="530.0" layoutY="250.0" prefWidth="200.0" />
<Label layoutX="363.0" layoutY="284.0" text="定位等操作最大等待时间 (毫秒):" />
<Slider layoutX="530.0" layoutY="280.0" prefWidth="200.0" />
<CheckBox layoutX="14.0" layoutY="314.0" mnemonicParsing="false" text="启用WASAPI" />
<CheckBox layoutX="14.0" layoutY="344.0" mnemonicParsing="false" text="启用独占模式" />
</children>
</AnchorPane>
</content>
</Tab>
<Tab text="媒体库">
<content>
<AnchorPane>
<children>
<CheckBox layoutX="14.0" layoutY="14.0" mnemonicParsing="false" text="禁用从磁盘删除" />
<CheckBox layoutX="14.0" layoutY="44.0" mnemonicParsing="false" text="将只有一项的分类归到其他类中" />
<Label layoutX="14.0" layoutY="74.0" text="音频文件低时长阈值 (秒):" />
<Slider layoutX="153.0" layoutY="70.0" prefWidth="200.0" />
<Label layoutX="363.0" layoutY="74.0" text="艺术家识别例外:" />
<TextField layoutX="530.0" layoutY="70.0" prefWidth="200.0" />
<CheckBox layoutX="14.0" layoutY="104.0" mnemonicParsing="false" text="移除不存在的音频文件" />
<CheckBox layoutX="14.0" layoutY="134.0" mnemonicParsing="false" text="忽略时长低于阈值的文件" />
<Button layoutX="530.0" layoutY="130.0" mnemonicParsing="false" text="强制重新加载" />
<Label layoutX="14.0" layoutY="164.0" text="媒体库目录" />
<TextField layoutX="153.0" layoutY="160.0" prefWidth="400.0" />
<Button layoutX="563.0" layoutY="160.0" mnemonicParsing="false" text="添加..." />
<Button layoutX="630.0" layoutY="160.0" mnemonicParsing="false" text="删除" />
<CheckBox layoutX="14.0" layoutY="194.0" mnemonicParsing="false" text="启动时自动更新媒体库" />
<CheckBox layoutX="14.0" layoutY="224.0" mnemonicParsing="false" text="在媒体库中显示的项目" />
<CheckBox layoutX="153.0" layoutY="224.0" mnemonicParsing="false" text="艺术家" />
<CheckBox layoutX="250.0" layoutY="224.0" mnemonicParsing="false" text="流派" />
<CheckBox layoutX="153.0" layoutY="254.0" mnemonicParsing="false" text="唱片集" />
<CheckBox layoutX="250.0" layoutY="254.0" mnemonicParsing="false" text="年份" />
<CheckBox layoutX="153.0" layoutY="284.0" mnemonicParsing="false" text="文件类型" />
<CheckBox layoutX="250.0" layoutY="284.0" mnemonicParsing="false" text="比特率" />
<CheckBox layoutX="153.0" layoutY="314.0" mnemonicParsing="false" text="分级" />
<CheckBox layoutX="250.0" layoutY="314.0" mnemonicParsing="false" text="所有曲目" />
<CheckBox layoutX="153.0" layoutY="344.0" mnemonicParsing="false" text="最近播放" />
<CheckBox layoutX="250.0" layoutY="344.0" mnemonicParsing="false" text="文件夹浏览" />
<CheckBox layoutX="14.0" layoutY="374.0" mnemonicParsing="false" text="播放列表选项" />
<CheckBox layoutX="153.0" layoutY="374.0" mnemonicParsing="false" text="禁用播放列表曲目拖放排序" />
<CheckBox layoutX="250.0" layoutY="374.0" mnemonicParsing="false" text="向播放列表中添加曲目时插入到开头而不是末尾" />
<CheckBox layoutX="14.0" layoutY="404.0" mnemonicParsing="false" text="默认使用浮动播放列表" />
<CheckBox layoutX="153.0" layoutY="404.0" mnemonicParsing="false" text="浮动播放列表跟随主窗口" />
</children>
</AnchorPane>
</content>
</Tab>
<Tab text="全局快捷键">
<content>
<AnchorPane style="-fx-background-color: #f0f0f0;">
<children>
<!-- 顶部复选框 -->
<CheckBox fx:id="enableShortcutsCheckBox" layoutX="14.0" layoutY="14.0"
selected="true" text="启用全局快捷键"/>
<!-- 快捷键表格 -->
<TableView fx:id="shortcutsTable" layoutX="14.0" layoutY="44.0"
prefHeight="450.0" prefWidth="780.0">
<columns>
<TableColumn fx:id="functionColumn" prefWidth="380.0" text="功能"/>
<TableColumn fx:id="shortcutColumn" prefWidth="300.0" text="快捷键"/>
</columns>
</TableView>
<!-- 新增的快捷键输入区域 -->
<HBox layoutX="14.0" layoutY="510.0" spacing="10" style="-fx-background-color: white; -fx-padding: 10;">
<Label text="修改快捷键:"/>
<TextField fx:id="shortcutInput" prefWidth="300.0" promptText="请按下快捷键组合"/>
<Button text="确认修改" onAction="#handleShortcutUpdate"
style="-fx-background-color: #4a90e2; -fx-text-fill: white;"/>
</HBox>
<!-- 底部按钮 -->
<HBox layoutX="540.0" layoutY="505.0" spacing="10">
<!-- <Button text="确定" onAction="#handleConfirm" style="-fx-min-width: 80;"/>-->
<!-- <Button text="取消" onAction="#handleCancel" style="-fx-min-width: 80;"/>-->
<!-- <Button text="应用" onAction="#handleApply" style="-fx-min-width: 80;"/>-->
<Button fx:id="restoreDefaultsButton" text="恢复默认" onAction="#handleRestoreDefaults" style="-fx-min-width: 80;"/>
<Button fx:id="applyButton" text="应用" onAction="#handleApply" style="-fx-min-width: 80;"/>
<!-- <Button fx:id="cancelButton" text="取消" onAction="#handleCancel" style="-fx-min-width: 80;"/>-->
<!-- <Button fx:id="confirmButton" text="确定" onAction="#handleConfirm" style="-fx-min-width: 80;"/>-->
</HBox>
<HBox layoutX="540.0" layoutY="535.0" spacing="10">
<Button fx:id="cancelButton" text="取消" onAction="#handleCancel" style="-fx-min-width: 80;"/>
<Button fx:id="confirmButton" text="确定" onAction="#handleConfirm" style="-fx-min-width: 80;"/>
</HBox>
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
</children>
</AnchorPane>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<!-- 文件日志 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/musicplayer.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/musicplayer.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>7</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别设置 -->
<logger name="com.musicPlayer" level="DEBUG"/>
<logger name="org.jaudiotagger" level="WARN"/> <!-- 音频标签库日志控制 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

@ -1,252 +0,0 @@
/* 全局样式 */
.root {
-fx-background-color: #f0f0f0; /* 浅灰色背景 */
-fx-text-fill: #333333; /* 深色字体 */
-fx-font-family: "Arial"; /* 使用常见的无衬线字体 */
}
/* 菜单栏样式 */
.menu-bar {
-fx-background-color: #e0e0e0; /* 稍深的灰色 */
}
.menu-bar .menu {
-fx-text-fill: #333333;
}
.menu-bar .menu-item {
-fx-background-color: #f0f0f0;
}
.menu-bar .menu-item:hover {
-fx-background-color: #d0d0d0;
}
/* 列表视图样式 */
.list-view {
-fx-background-color: #ffffff; /* 白色背景 */
-fx-border-color: #d0d0d0; /* 浅边框 */
-fx-border-radius: 5px;
-fx-background-radius: 5px;
}
.list-view .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #333333;
-fx-padding: 8px 16px 8px 16px; /* 左右内边距,去除白边 */
-fx-background-radius: 10px;
-fx-border-radius: 10px;
-fx-border-width: 0;
-fx-alignment: center-left;
-fx-effect: none;
}
.list-view .list-cell:selected {
-fx-background-color: #d0e0f0;
-fx-background-radius: 10px;
}
.list-view .list-cell:hover {
-fx-background-color: #e9e9e9;
-fx-background-radius: 10px;
}
.song-label {
-fx-font-size: 14px;
-fx-text-fill: #222;
-fx-padding: 0 0 0 8px;
-fx-alignment: center-left;
}
.delete-btn {
-fx-opacity: 0;
-fx-cursor: hand;
-fx-background-color: transparent;
-fx-padding: 0 8 0 8;
-fx-border-width: 0;
-fx-alignment: center-right;
-fx-effect: none;
-fx-translate-x: 0;
-fx-translate-y: 0;
-fx-background-radius: 50%;
-fx-min-width: 32px;
-fx-min-height: 32px;
-fx-max-width: 32px;
-fx-max-height: 32px;
}
.list-cell:hover .delete-btn {
-fx-opacity: 1;
-fx-transition: opacity 0.2s;
}
/* HBox美化左右分布 */
.list-view .hbox {
-fx-spacing: 10px;
-fx-alignment: center-left;
-fx-pref-width: 100%;
}
/* 歌词 ListView 样式 */
.lyrics-view {
-fx-background-color: #ffffff;
-fx-border-color: #d0d0d0;
-fx-border-radius: 5px;
-fx-background-radius: 5px;
-fx-padding: 10px; /* 添加内边距 */
}
.lyrics-view .list-cell {
-fx-background-color: transparent;
-fx-text-fill: #555555; /* 歌词文本颜色 */
-fx-padding: 2px 5px; /* 调整内边距 */
-fx-alignment: center; /* 歌词居中显示 */
-fx-font-size: 14px; /* 调整字体大小 */
}
/* 当前歌词行高亮样式 */
.lyrics-view .list-cell.current-lyric-line {
-fx-text-fill: #4CAF50; /* 高亮颜色 */
-fx-font-weight: bold; /* 加粗 */
}
/* 按钮样式 */
.button {
-fx-background-color: #e0e0e0;
-fx-text-fill: #333333;
-fx-padding: 8px 16px;
-fx-background-radius: 20px; /* 圆角 */
-fx-cursor: hand; /* 手形光标 */
}
.button:hover {
-fx-background-color: #d0d0d0;
}
.button:pressed {
-fx-background-color: #c0c0c0;
}
.control-button {
-fx-background-radius: 50%; /* 使播放控制按钮圆形 */
-fx-min-width: 40px;
-fx-min-height: 40px;
-fx-max-width: 40px;
-fx-max-height: 40px;
-fx-padding: 0;
-fx-alignment: center;
}
.control-button:hover {
-fx-background-color: #d0d0d0;
}
.control-button:pressed {
-fx-background-color: #c0c0c0;
}
/* 播放/暂停按钮特殊样式 */
#playPauseButton {
-fx-background-color: #4CAF50; /* 绿色 */
-fx-text-fill: #ffffff;
}
#playPauseButton:hover {
-fx-background-color: #45a049;
}
#playPauseButton:pressed {
-fx-background-color: #39843c;
}
/* 滑块样式 */
.slider {
-fx-padding: 5px 0;
}
.slider .track {
-fx-background-color: #d0d0d0;
-fx-pref-height: 5px; /* 调整滑轨高度 */
-fx-background-radius: 5px;
}
.slider .thumb {
-fx-background-color: #4CAF50; /* 绿色 */
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 5, 0, 0, 1); /* 添加阴影 */
}
/* 标签样式 */
.label {
-fx-text-fill: #333333;
}
/* 分割面板样式 */
.split-pane {
-fx-background-color: #f0f0f0;
-fx-divider-position: 0.3;
}
.split-pane-divider {
-fx-background-color: #c0c0c0; /* 分隔线颜色 */
-fx-pref-width: 1px; /* 分隔线宽度 */
}
/* 播放控制区域样式 */
.control-panel {
-fx-background-color: #e9e9e9; /* 浅灰色背景 */
-fx-padding: 15px;
-fx-spacing: 20px;
-fx-alignment: center; /* 居中 */
-fx-background-radius: 10px;
}
/* 专辑封面样式 */
.album-cover {
-fx-background-color: #d0d0d0; /* 占位背景 */
-fx-min-width: 200px;
-fx-min-height: 200px;
-fx-max-width: 200px;
-fx-max-height: 200px;
-fx-background-radius: 5px; /* 轻微圆角 */
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.5, 0, 5); /* 添加阴影 */
}
/* 信息面板样式 */
.info-panel {
-fx-background-color: #e9e9e9;
-fx-padding: 20px;
-fx-spacing: 20px;
-fx-alignment: center_left;
-fx-background-radius: 10px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.1), 5, 0, 0, 2);
}
/* 播放列表面板样式 */
.playlist-panel {
-fx-background-color: #e9e9e9;
-fx-padding: 10px;
-fx-spacing: 10px;
-fx-background-radius: 10px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.1), 5, 0, 0, 2);
}
/* 进度条当前时间/总时间标签样式 */
#timeLabel {
-fx-font-size: 14px;
-fx-text-fill: #555555;
-fx-min-width: 80px;
-fx-alignment: center;
}
/* 滚动条样式 */
.scroll-bar {
-fx-background-color: #2b2b2b;
}
.scroll-bar .thumb {
-fx-background-color: #4a4a4a;
}
.scroll-bar .thumb:hover {
-fx-background-color: #5a5a5a;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save