Compare commits

..

No commits in common. "main" and "main_Python" have entirely different histories.

15 changed files with 438 additions and 3016 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
/target

2367
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
[package]
name = "DreamLife_MusicPlayer"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
xml = "0.8"
rodio = "0.19"
base64 = "0.22"
dirs = "5"
futures = "0.3"
urlencoding = "2"

1
README.md Normal file
View File

@ -0,0 +1 @@
<h1 align=center>DreamLife|MusicPlayer</h1>

BIN
file/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

BIN
file/next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
file/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
file/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
file/prev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

302
musicplayer.py Normal file
View File

@ -0,0 +1,302 @@
import sys
import os
import pygame
import time
import re
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel,
QPushButton, QSlider, QHBoxLayout, QFileDialog
)
from PyQt6.QtCore import QTimer, Qt, QSize, QPropertyAnimation, QPoint
from PyQt6.QtGui import QPixmap, QIcon, QPalette, QColor
class MusicPlayer(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("DreamLife|MusicPlayer")
self.setGeometry(300, 200, 400, 500)
self.initUI()
pygame.mixer.init()
self.playlist = [] # 歌曲列表(文件路径)
self.current_song_index = -1 # 当前播放歌曲索引
self.lyrics = [] # 解析后的歌词列表 [(时间戳, 歌词)]
self.current_lyric_index = 0 # 当前歌词索引
self.is_playing = False # 播放状态
self.music_length = 0 # 音乐总时长(秒)
self.start_time = 0 # 播放开始时间(用于计算已播放时间)
self.pause_time = 0 # 暂停时记录的播放进度(秒)
self.animation = None # 存储滚动动画对象
self.last_lyric = None # 上一次显示的歌词,用于避免重复启动动画
self.music_file = None # 当前音乐文件路径
self.lyrics_file = None # 当前歌词文件路径
self.cover_file = None # 当前封面文件路径
def initUI(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout()
self.layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.central_widget.setLayout(self.layout)
# 设置背景颜色
self.set_background_color("#2E2E2E")
# 封面
self.cover_label = QLabel()
self.layout.addWidget(self.cover_label, alignment=Qt.AlignmentFlag.AlignCenter)
# 添加一个按钮用于加载外部音乐文件夹
self.load_folder_button = QPushButton("选择音乐文件夹")
self.load_folder_button.clicked.connect(self.open_folder_dialog)
self.layout.addWidget(self.load_folder_button, alignment=Qt.AlignmentFlag.AlignCenter)
# 歌词显示区域(固定大小容器,便于动画处理,不随内容改变大小)
self.lyrics_container = QWidget()
self.lyrics_container.setFixedSize(300, 50)
self.lyrics_container.setStyleSheet("background-color: transparent;")
# 歌词标签放置在容器中
self.lyrics_browser = QLabel("", self.lyrics_container)
self.lyrics_browser.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.lyrics_browser.setStyleSheet("font-size: 24px; color: white; font-weight: bold;")
self.lyrics_browser.setFixedHeight(50)
self.lyrics_browser.move(0, 0)
self.layout.addWidget(self.lyrics_container, alignment=Qt.AlignmentFlag.AlignCenter)
# 进度条
self.slider = QSlider(Qt.Orientation.Horizontal)
self.slider.setRange(0, 100)
self.slider.sliderPressed.connect(self.pause_music)
self.slider.sliderReleased.connect(self.seek_music)
self.layout.addWidget(self.slider)
# 播放控制区域(增加上一首、播放/暂停、下一首)
self.control_layout = QHBoxLayout()
self.control_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.prev_button = QPushButton()
self.prev_button.setIcon(QIcon(self.resource_path("file/prev.png")))
self.prev_button.setIconSize(QSize(40, 40))
self.prev_button.setStyleSheet("border: none;")
self.prev_button.clicked.connect(self.play_previous_song)
self.control_layout.addWidget(self.prev_button)
self.play_button = QPushButton()
self.play_button.setIcon(QIcon(self.resource_path("file/play.png")))
self.play_button.setIconSize(QSize(52, 52))
self.play_button.setStyleSheet("border: none;")
self.play_button.clicked.connect(self.toggle_play_pause)
self.control_layout.addWidget(self.play_button)
self.next_button = QPushButton()
self.next_button.setIcon(QIcon(self.resource_path("file/next.png")))
self.next_button.setIconSize(QSize(40, 40))
self.next_button.setStyleSheet("border: none;")
self.next_button.clicked.connect(self.play_next_song)
self.control_layout.addWidget(self.next_button)
self.layout.addLayout(self.control_layout)
# 定时器每500毫秒更新一次进度和歌词
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_lyrics_and_progress)
def resource_path(self, relative_path):
""" 获取资源文件的路径,适配 PyInstaller 打包模式 """
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS # PyInstaller 运行时解压目录
else:
base_path = os.path.dirname(os.path.abspath(__file__)) # 开发模式
return os.path.join(base_path, relative_path)
def set_background_color(self, color):
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, QColor(color))
self.setPalette(palette)
self.central_widget.setStyleSheet(f"background-color: {color};")
def load_cover(self):
# 如果用户选择了封面文件,优先加载,否则加载默认封面
if self.cover_file and os.path.exists(self.cover_file):
pixmap = QPixmap(self.cover_file)
else:
cover_path = self.resource_path("file/cover.jpg")
if os.path.exists(cover_path):
pixmap = QPixmap(cover_path)
else:
self.cover_label.setText("封面未找到")
return
self.cover_label.setPixmap(pixmap.scaled(256, 256, Qt.AspectRatioMode.KeepAspectRatio))
def load_lyrics(self):
# 根据用户选择的歌词文件加载歌词,如果没有选择则使用默认歌词文件
if self.lyrics_file and os.path.exists(self.lyrics_file):
path = self.lyrics_file
else:
# 默认歌词文件(可以根据需要调整)
path = self.resource_path("file/默认歌词.lrc")
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8') as file:
lines = file.readlines()
self.lyrics = []
for line in lines:
match = re.match(r"\[(\d+):(\d+\.\d+)](.*)", line)
if match:
minutes = int(match.group(1))
seconds = float(match.group(2))
timestamp = minutes * 60 + seconds
lyrics_text = match.group(3).strip()
self.lyrics.append((timestamp, lyrics_text))
self.lyrics.sort()
except Exception as e:
self.lyrics_browser.setText("加载歌词出错: " + str(e))
else:
self.lyrics_browser.setText("歌词文件未找到")
def open_folder_dialog(self):
folder_path = QFileDialog.getExistingDirectory(self, "选择音乐文件夹", "")
if folder_path:
# 读取该文件夹下所有音乐文件支持wav和mp3
self.playlist = []
for file in os.listdir(folder_path):
if file.lower().endswith(('.wav', '.mp3')):
self.playlist.append(os.path.join(folder_path, file))
self.playlist.sort() # 可按文件名排序
if self.playlist:
# 将当前索引设置为第一首
self.current_song_index = 0
self.load_current_song_resources()
self.play_music()
else:
self.lyrics_browser.setText("文件夹中没有找到音乐文件")
def load_current_song_resources(self):
# 设置当前音乐文件路径及相关资源(歌词和封面)
self.music_file = self.playlist[self.current_song_index]
base, _ = os.path.splitext(self.music_file)
lyrics_candidate = base + ".lrc"
cover_candidate = base + ".jpg"
self.lyrics_file = lyrics_candidate if os.path.exists(lyrics_candidate) else None
self.cover_file = cover_candidate if os.path.exists(cover_candidate) else None
self.load_cover()
self.load_lyrics()
def toggle_play_pause(self):
if not self.is_playing:
if self.pause_time > 0:
self.resume_music()
else:
self.play_music()
else:
self.pause_music()
def play_music(self):
if self.music_file and os.path.exists(self.music_file):
try:
pygame.mixer.music.load(self.music_file)
pygame.mixer.music.play(start=0)
self.music_length = pygame.mixer.Sound(self.music_file).get_length()
self.start_time = time.time()
self.pause_time = 0
self.timer.start(500)
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
self.is_playing = True
except Exception as e:
self.lyrics_browser.setText("播放错误: " + str(e))
else:
self.lyrics_browser.setText("")
def resume_music(self):
try:
pygame.mixer.music.unpause()
self.start_time = time.time() - self.pause_time
self.timer.start(500)
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
self.is_playing = True
except Exception as e:
self.lyrics_browser.setText("恢复播放错误: " + str(e))
def pause_music(self):
if self.is_playing:
self.pause_time = time.time() - self.start_time
pygame.mixer.music.pause()
self.timer.stop()
self.play_button.setIcon(QIcon(self.resource_path("file/play.png")))
self.is_playing = False
def seek_music(self):
if self.music_length > 0:
new_time = self.slider.value() / 100 * self.music_length
pygame.mixer.music.play(start=new_time)
self.start_time = time.time() - new_time
self.pause_time = new_time
self.current_lyric_index = self.find_lyric_index(new_time)
self.update_lyrics_display()
self.timer.start(500)
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
self.is_playing = True
def play_next_song(self):
if self.playlist:
self.current_song_index = (self.current_song_index + 1) % len(self.playlist)
self.load_current_song_resources()
self.play_music()
def play_previous_song(self):
if self.playlist:
self.current_song_index = (self.current_song_index - 1) % len(self.playlist)
self.load_current_song_resources()
self.play_music()
def find_lyric_index(self, current_time):
for i, (timestamp, _) in enumerate(self.lyrics):
if current_time < timestamp:
return max(i - 1, 0)
return len(self.lyrics) - 1
def update_lyrics_display(self):
if not self.lyrics:
self.lyrics_browser.setText("歌词为空")
return
if self.current_lyric_index < len(self.lyrics):
_, lyric = self.lyrics[self.current_lyric_index]
if self.last_lyric != lyric:
self.last_lyric = lyric
if self.lyrics_browser.fontMetrics().horizontalAdvance(lyric) > self.lyrics_container.width():
self.scroll_lyrics(lyric)
else:
if self.animation is not None:
self.animation.stop()
self.animation = None
self.lyrics_browser.setText(lyric)
self.lyrics_browser.adjustSize()
self.lyrics_browser.move((self.lyrics_container.width() - self.lyrics_browser.width()) // 2, 0)
def scroll_lyrics(self, lyric):
self.lyrics_browser.setText(lyric)
self.lyrics_browser.adjustSize()
label_width = self.lyrics_browser.width()
container_width = self.lyrics_container.width()
start_pos = QPoint(50, 0)
end_pos = QPoint(-label_width, 0)
if self.animation is not None:
self.animation.stop()
self.animation = QPropertyAnimation(self.lyrics_browser, b"pos")
duration = 8000 + (label_width - container_width) * 20
self.animation.setDuration(duration)
self.animation.setStartValue(start_pos)
self.animation.setEndValue(end_pos)
self.animation.start()
def update_lyrics_and_progress(self):
elapsed_time = time.time() - self.start_time
self.current_lyric_index = self.find_lyric_index(elapsed_time)
self.update_lyrics_display()
if self.music_length > 0:
progress = int((elapsed_time / self.music_length) * 100)
self.slider.setValue(progress)
if __name__ == '__main__':
app = QApplication(sys.argv)
player = MusicPlayer()
player.show()
sys.exit(app.exec())

View File

@ -1,632 +0,0 @@
use base64::Engine;
use reqwest::{header::{HeaderMap, HeaderValue}, Client, ClientBuilder};
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use urlencoding;
use xml::reader::{EventReader, XmlEvent};
const MUSIC_EXTENSIONS: &[&str] = &[
"mp3", "flac", "wav", "ogg", "m4a", "aac", "opus", "ape", "wma",
];
#[derive(Debug, Clone)]
struct WebDavFile {
name: String,
path: String,
is_directory: bool,
#[allow(dead_code)]
size: Option<u64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Config {
server: String,
username: String,
password: String,
}
impl Config {
fn from_file(path: &PathBuf) -> Option<Self> {
let content = std::fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
Some(Config {
server: json["server"].as_str()?.to_string(),
username: json["username"].as_str()?.to_string(),
password: json["password"].as_str()?.to_string(),
})
}
fn save(&self, path: &PathBuf) -> Result<(), String> {
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
std::fs::write(path, json).map_err(|e| e.to_string())?;
Ok(())
}
}
struct AudioPlayer {
_stream: OutputStream,
_stream_handle: OutputStreamHandle,
sink: Sink,
}
impl AudioPlayer {
fn new() -> Self {
let (stream, stream_handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&stream_handle).unwrap();
Self {
_stream: stream,
_stream_handle: stream_handle,
sink,
}
}
fn play(&self, file_path: &PathBuf) -> Result<(), String> {
let file = File::open(file_path).map_err(|e| e.to_string())?;
let source = Decoder::new(BufReader::new(file)).map_err(|e| e.to_string())?;
self.sink.append(source);
Ok(())
}
fn pause(&self) {
self.sink.pause();
}
fn resume(&self) {
self.sink.play();
}
fn stop(&self) {
self.sink.stop();
}
fn is_paused(&self) -> bool {
self.sink.is_paused()
}
fn is_empty(&self) -> bool {
self.sink.empty()
}
}
struct AppState {
client: Client,
config: Config,
current_path: String,
playlist: VecDeque<WebDavFile>,
current_index: usize,
}
impl AppState {
fn new(config: Config) -> Result<Self, String> {
let auth = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", config.username, config.password));
let mut headers = HeaderMap::new();
headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", auth)).unwrap());
let client = ClientBuilder::new()
.timeout(Duration::from_secs(30))
.default_headers(headers)
.build()
.map_err(|e| e.to_string())?;
Ok(Self {
client,
config,
current_path: "/".to_string(),
playlist: VecDeque::new(),
current_index: 0,
})
}
fn is_music_file(name: &str) -> bool {
let lower = name.to_lowercase();
MUSIC_EXTENSIONS.iter().any(|ext| lower.ends_with(&format!(".{}", ext)))
}
async fn list_directory(&self, path: &str) -> Result<Vec<WebDavFile>, String> {
// Encode the path to handle Chinese/special characters
let encoded_path = encode_path(path);
let url = format!("{}{}", self.config.server.trim_end_matches('/'), encoded_path);
let body = r#"<?xml version="1.0" encoding="utf-8"?>
<d:propfind xmlns:d="DAV:">
<d:allprop />
</d:propfind>"#;
let response = self.client
.request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), &url)
.header("Depth", "1")
.header("Content-Type", "text/xml; charset=utf-8")
.body(body)
.send()
.await
.map_err(|e| e.to_string())?;
let status = response.status();
if !status.is_success() {
return Err(format!("HTTP error: {} - {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown")));
}
let text = response.text().await.map_err(|e| e.to_string())?;
let files = self.parse_webdav_response(&text, path);
Ok(files)
}
fn get_file_url(&self, file_path: &str) -> String {
let encoded_path = encode_path(file_path);
format!("{}{}", self.config.server.trim_end_matches('/'), encoded_path)
}
fn parse_webdav_response(&self, xml: &str, current_path: &str) -> Vec<WebDavFile> {
let reader = EventReader::from_str(xml);
let mut files = Vec::new();
let mut in_response = false;
let mut in_href = false;
let mut in_collection = false;
let mut current_href = String::new();
for event in reader.into_iter() {
match event {
Ok(XmlEvent::StartElement { name, .. }) => {
let local_name = name.local_name.as_str();
if local_name == "response" {
in_response = true;
}
if in_response && local_name == "href" {
in_href = true;
}
if in_response && local_name == "collection" {
in_collection = true;
}
}
Ok(XmlEvent::EndElement { name }) => {
let local_name = name.local_name.as_str();
if local_name == "response" && in_response {
if !current_href.is_empty() {
let clean_path = current_href.trim_end_matches('/');
// URL decode the path to handle Chinese characters
let decoded_path: String = urlencoding::decode(clean_path).unwrap_or_else(|_| std::borrow::Cow::Owned(clean_path.to_string())).to_string();
let filename = PathBuf::from(&decoded_path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| decoded_path.clone());
let full_path = if decoded_path.starts_with('/') {
decoded_path
} else {
format!("/{}", decoded_path)
};
if full_path != current_path && !filename.is_empty() {
files.push(WebDavFile {
name: filename,
path: full_path,
is_directory: in_collection,
size: None,
});
}
}
in_response = false;
in_href = false;
in_collection = false;
current_href = String::new();
}
if local_name == "href" {
in_href = false;
}
}
Ok(XmlEvent::Characters(s)) => {
if in_href && in_response {
current_href.push_str(&s);
}
}
Err(_) => {}
_ => {}
}
}
files.sort_by(|a, b| {
if a.is_directory != b.is_directory {
b.is_directory.cmp(&a.is_directory)
} else {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
}
});
files
}
fn build_playlist(&mut self, files: Vec<WebDavFile>) {
self.playlist.clear();
for file in files {
if !file.is_directory && Self::is_music_file(&file.name) {
self.playlist.push_back(file);
}
}
self.current_index = 0;
}
}
fn encode_path(path: &str) -> String {
// Split path into segments and encode each segment
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let encoded_segments: Vec<String> = segments.iter().map(|s| urlencoding::encode(s).to_string()).collect();
format!("/{}", encoded_segments.join("/"))
}
async fn download_and_play(
state: &AppState,
file: &WebDavFile,
player: &AudioPlayer,
) -> Result<(), String> {
let url = state.get_file_url(&file.path);
let response = state
.client
.get(&url)
.send()
.await
.map_err(|e| e.to_string())?;
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join(format!("dlmp_{}", file.name.replace('/', "_")));
std::fs::write(&temp_file, &bytes).map_err(|e| e.to_string())?;
player.play(&temp_file)?;
let _ = std::fs::remove_file(&temp_file);
Ok(())
}
fn prompt_input(prompt: &str) -> String {
print!("{}", prompt);
std::io::stdout().flush().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}
async fn prompt_for_config() -> Config {
println!("\nPlease enter your WebDAV configuration:");
let server = prompt_input("Server URL: ");
let username = prompt_input("Username: ");
let password = prompt_input("Password: ");
Config { server, username, password }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("========================================");
println!(" DreamLife Music Player - WebDAV");
println!("========================================\n");
let config_path = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("DreamLife_MusicPlayer")
.join("config.json");
let config = match Config::from_file(&config_path) {
Some(c) => {
println!("Loaded config from: {:?}", config_path);
c
}
None => {
println!("Config file not found at: {:?}", config_path);
let config = prompt_for_config().await;
match config.save(&config_path) {
Ok(_) => println!("Config saved to: {:?}", config_path),
Err(e) => println!("Warning: Could not save config: {}", e),
}
config
}
};
println!("\nConnecting to: {}", config.server);
let state = Arc::new(Mutex::new(AppState::new(config)?));
let player = Arc::new(Mutex::new(AudioPlayer::new()));
let config_path_clone = Arc::new(config_path.clone());
loop {
let current_path = {
let state_guard = state.lock().await;
state_guard.current_path.clone()
};
println!("\n[Current Path: {}]", current_path);
println!("Commands: ls, cd, play, queue, stop, pause, resume, next, prev, config, quit\n");
print!("> ");
std::io::Write::flush(&mut std::io::stdout())?;
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
break;
}
let input = input.trim();
if input.is_empty() {
continue;
}
let parts: Vec<&str> = input.splitn(2, ' ').collect();
let cmd = parts[0].to_lowercase();
let arg = parts.get(1).map(|s| s.to_string());
match cmd.as_str() {
"ls" | "dir" => {
let path = match arg {
Some(ref p) => {
if p.starts_with('/') {
p.clone()
} else {
let base = state.lock().await.current_path.clone();
if base == "/" {
format!("/{}", p)
} else {
format!("{}/{}", base, p)
}
}
}
None => state.lock().await.current_path.clone(),
};
match state.lock().await.list_directory(&path).await {
Ok(files) => {
println!("\n [DIRS]");
for f in files.iter().filter(|f| f.is_directory) {
println!(" [DIR] {}", f.name);
}
println!("\n [FILES]");
for f in files.iter().filter(|f| !f.is_directory) {
let marker = if AppState::is_music_file(&f.name) { "*" } else { "" };
println!(" {} {}", marker, f.name);
}
}
Err(e) => println!("Error: {}", e),
}
}
"cd" => {
if let Some(path) = arg {
let new_path = {
let state_guard = state.lock().await;
if path == ".." {
let current = &state_guard.current_path;
if current == "/" {
"/".to_string()
} else {
let p = current.trim_end_matches('/');
if let Some(pos) = p.rfind('/') {
if pos == 0 { "/".to_string() } else { p[..pos].to_string() }
} else {
"/".to_string()
}
}
} else if path.starts_with('/') {
path
} else {
let base = &state_guard.current_path;
if base == "/" {
format!("/{}", path)
} else {
format!("{}/{}", base, path)
}
}
};
state.lock().await.current_path = new_path;
} else {
println!("Usage: cd <path>");
}
}
"play" => {
if let Some(filename) = arg {
let (_path, file_to_play) = {
let state_guard = state.lock().await;
let path = if filename.starts_with('/') {
filename.clone()
} else {
let base = &state_guard.current_path;
if base == "/" {
format!("/{}", filename)
} else {
format!("{}/{}", base, filename)
}
};
let file = WebDavFile {
name: filename.clone(),
path: path.clone(),
is_directory: false,
size: None,
};
(path, file)
};
println!("Playing: {}", filename);
let state_clone = state.lock().await;
let player_clone = player.lock().await;
if let Err(e) = download_and_play(&state_clone, &file_to_play, &player_clone).await {
println!("Error playing: {}", e);
} else {
println!("Now playing: {}", filename);
}
} else {
println!("Usage: play <filename>");
}
}
"queue" => {
let path = {
let state_guard = state.lock().await;
match arg {
Some(ref p) => {
if p.starts_with('/') {
p.clone()
} else {
let base = &state_guard.current_path;
if base == "/" {
format!("/{}", p)
} else {
format!("{}/{}", base, p)
}
}
}
None => state_guard.current_path.clone(),
}
};
match state.lock().await.list_directory(&path).await {
Ok(files) => {
state.lock().await.build_playlist(files);
let count = state.lock().await.playlist.len();
println!("Added {} music files to playlist", count);
if count > 0 {
let file = state.lock().await.playlist.front().unwrap().clone();
let state_guard = state.lock().await;
let player_guard = player.lock().await;
if let Err(e) = download_and_play(&state_guard, &file, &player_guard).await {
println!("Error: {}", e);
} else {
println!("Playing: {}", file.name);
}
}
}
Err(e) => println!("Error: {}", e),
}
}
"stop" => {
player.lock().await.stop();
println!("Stopped");
}
"pause" => {
player.lock().await.pause();
println!("Paused");
}
"resume" => {
player.lock().await.resume();
println!("Resumed");
}
"next" => {
let (should_play, file) = {
let mut state_guard = state.lock().await;
if state_guard.playlist.is_empty() {
(false, None)
} else {
state_guard.current_index = (state_guard.current_index + 1) % state_guard.playlist.len();
let file = state_guard.playlist.get(state_guard.current_index).unwrap().clone();
(true, Some(file))
}
};
if should_play {
if let Some(file) = file {
player.lock().await.stop();
let state_guard = state.lock().await;
let player_guard = player.lock().await;
if let Err(e) = download_and_play(&state_guard, &file, &player_guard).await {
println!("Error: {}", e);
} else {
println!("Playing: {}", file.name);
}
}
} else {
println!("Playlist is empty");
}
}
"prev" => {
let (should_play, file) = {
let mut state_guard = state.lock().await;
if state_guard.playlist.is_empty() {
(false, None)
} else {
if state_guard.current_index == 0 {
state_guard.current_index = state_guard.playlist.len() - 1;
} else {
state_guard.current_index -= 1;
}
let file = state_guard.playlist.get(state_guard.current_index).unwrap().clone();
(true, Some(file))
}
};
if should_play {
if let Some(file) = file {
player.lock().await.stop();
let state_guard = state.lock().await;
let player_guard = player.lock().await;
if let Err(e) = download_and_play(&state_guard, &file, &player_guard).await {
println!("Error: {}", e);
} else {
println!("Playing: {}", file.name);
}
}
} else {
println!("Playlist is empty");
}
}
"status" => {
let p = player.lock().await;
if p.is_empty() {
println!("No music playing");
} else if p.is_paused() {
println!("Paused");
} else {
println!("Playing");
}
}
"config" => {
let new_config = prompt_for_config().await;
match new_config.save(&config_path_clone) {
Ok(_) => println!("Config saved successfully"),
Err(e) => println!("Warning: Could not save config: {}", e),
}
*state.lock().await = AppState::new(new_config.clone())?;
println!("Connected to new server: {}", new_config.server);
}
"quit" | "exit" => {
println!("Goodbye!");
break;
}
_ => {
println!("Unknown command: {}", cmd);
println!("Available: ls, cd, play, queue, stop, pause, resume, next, prev, status, config, quit");
}
}
}
Ok(())
}

View File

@ -0,0 +1,62 @@
[00:00.00] 曲 COMPOSER : 林俊杰
[00:01.00] 词 LYRICS : 易家扬
[00:02.00] 编曲 MUSIC ARRANGEMENT : 林俊杰
[00:03.00] 制作人 : 林俊杰
[00:21.86]
[00:24.38]星空拉着路人 记忆碰撞年轮
[00:29.48]最后一圈 往前奔
[00:35.95]月晕下的孤魂 被过去戳的好疼
[00:41.94]看来时路出神
[00:45.12]
[00:48.54]人在赛道跑着 撑着 争着 忍着
[00:51.49]心在黑里跪着 吼着 问着 等着
[00:53.84]你在人海游着 抖着 躲着 沉着
[00:57.49]我呢 目送着那些痴愚瞋
[01:01.25]跟自己对峙过了 就别闹了 别复制问号 这胜负已分
[01:11.64]
[01:13.27]等光阴的副本孤单又安静在天上呼唤二次人生
[01:18.77]等翻页了之后遗憾和暗黑的物质我得让它滚
[01:24.85]等穿越了无声的冰川无言的低谷绿芽败中求胜
[01:31.60]我加上我们
[01:34.97]
[01:37.15]拿那光阴的副本看看我还有 多少个不朽或是永恒
[01:43.48]我是个到终点然而又要起跑的人
[01:49.54]不怕未来路上愤怒的雷神
[01:55.84]我保护我们
[01:58.95]
[02:27.31]另外一次如果 另外一次结果 另外一次 假如我
[02:38.34]那时的惊叹号 那些梦真没老 等我来要
[02:48.48]
[02:51.34]等光阴的副本忽然说暂停的人生之后还有人生
[02:57.46]等交换了所有苦闷给苍白的世界又能打几分
[03:03.19]等听多了无声的世界无言的世间有笑也有引恨
[03:09.28]我怀念我们
[03:13.03]
[03:15.74]拿那光阴的副本看看我那些不忘的不退或是不肯
[03:22.60]我输过 是没错 哼 然而我 还没认
[03:28.24]多少哑口无语 只为听一声
[03:34.58]我很爱我们
[03:38.78]
[04:07.62] 曲 COMPOSER: 林俊杰 JJ LIN
[04:07.84] 词 LYRICS: 易家扬
[04:08.06]
[04:08.27] 制作人 PRODUCER: 林俊杰 JJ LIN
[04:08.49] 配唱制作 VOCAL PRODUCTION: 林俊杰 JJ LIN
[04:08.71] 制作协力 PRODUCTION ASSISTANCE: 黄冠龙 ALEX.D / 周信廷 SHiN CHOU / 蔡凯升 Kai Tsai
[04:08.92]
[04:09.14] 编曲 MUSIC ARRANGEMENT: 林俊杰 JJ LIN
[04:09.36] 吉他 GUITAR黄冠龙 ALEX.D
[04:09.57] 低音吉他 BASS GUITAR: Andy Peterson
[04:09.79] 鼓 DRUMSAsh Soan
[04:10.01] 大提琴 CELLO庄家欢 Olivia Chuang
[04:10.23] 弦乐 STRINGS国际首席爱乐乐团
[04:10.44] 和声编写 BACKGROUND VOCAL ARRANGEMENT: 林俊杰 JJ LIN
[04:10.66] 和声 BACKGROUND VOCALS: 林俊杰 JJ LIN
[04:10.88]
[04:11.09] 录音室 RECORDING STUDIO: THE JFJ BLUE ROOM (Singapore) / ALEX.D Studio (Taipei) / Crosstown studio (Malaysia) / The Windmill Studio (Norfolk,England) / IdeaNique Studio (Singapore) / 中国剧院录 音棚 (Beijing)
[04:11.31] 录音师 RECORDING ENGINEER: 林俊杰 JJ LIN / 黄冠龙ALEX.D / Ananth / Ash Soan / 洪俊扬 JY / 陈子健 ZJ / 李巍
[04:11.53]
[04:11.74] 混音室 MIXING STUDIO: mixHaus (Encino, CA)
[04:11.96] 混音师 MIXING ENGINEER: Richard Furch
[04:12.18] 后期母带处理制作人 MASTERING PRODUCER: 林俊杰 JJ LIN
[04:12.40] 后期母带处理录音室 MASTERING STUDIO: Bernie Grundman Mastering, LA

Binary file not shown.

View File

@ -0,0 +1,73 @@
[ti:裂缝中的阳光]
[ar:林俊杰]
[al:因你而在]
[by:]
[offset:0]
[00:00.00]裂缝中的阳光 - 林俊杰 (JJ Lin)
[00:04.25]词:吴青峰
[00:08.51]曲:林俊杰
[00:12.76]编曲:林俊杰
[00:17.02]制作人:林俊杰
[00:21.28]有多少创伤卡在咽喉
[00:24.27]
[00:26.76]有多少眼泪滴湿枕头
[00:29.84]
[00:32.38]有多少你觉得不能够
[00:35.38]
[00:36.66]被懂的痛 只能沉默
[00:41.13]
[00:43.68]有多少夜晚没有尽头
[00:49.15]有多少的寂寞 无人诉说
[00:54.85]有多少次的梦 还没做 已成空
[01:06.11]等到黑夜翻面之后
[01:08.98]会是新的白昼
[01:11.74]等到海啸退去之后
[01:14.62]只是潮起潮落
[01:18.11]别到最后你才发觉
[01:21.59]心里头的野兽
[01:24.53]还没到最终就已经罢休
[01:28.83]心脏没有那么脆弱
[01:31.62]总还会有执着
[01:33.64]
[01:34.43]人生不会只有收获
[01:37.21]总难免有伤口
[01:39.91]
[01:40.66]不要害怕生命中
[01:43.45]
[01:44.30]不完美的角落
[01:46.35]
[01:47.09]阳光在每个裂缝中散落
[02:00.78]呜呜
[02:09.34]Yeah
[02:16.70]就算一切重来又怎样
[02:21.41]
[02:22.47]让你的心在我 心上跳动
[02:28.02]每个逐渐暗下来的夜 一起走过
[02:37.25]
[02:39.36]等到黑夜翻面之后
[02:42.24]会是新的白昼
[02:44.17]
[02:45.07]等到海啸退去之后
[02:47.80]只是潮起潮落
[02:50.21]
[02:51.41]别到最后你才发觉
[02:53.93]
[02:54.87]心里头的野兽
[02:57.00]
[02:57.72]还没到最终就已经罢休
[03:01.93]心脏没有那么脆弱
[03:04.69]总还会有执着
[03:06.81]
[03:07.60]人生不会只有收获
[03:10.36]总难免有伤口
[03:12.93]
[03:13.83]不要害怕生命中
[03:16.57]
[03:17.47]不完美的角落
[03:19.55]
[03:20.27]阳光在每个裂缝中散落
[03:25.48]
[03:28.17]不如就勇敢打破
[03:30.95]
[03:31.62]生命中的裂缝
[03:34.46]阳光就逐渐洒满了其中

Binary file not shown.