Compare commits
No commits in common. "main" and "main_Python" have entirely different histories.
main
...
main_Pytho
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
2367
Cargo.lock
generated
2367
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@ -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"
|
|
||||||
BIN
file/cover.jpg
Normal file
BIN
file/cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 582 KiB |
BIN
file/next.png
Normal file
BIN
file/next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
file/pause.png
Normal file
BIN
file/pause.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
file/play.png
Normal file
BIN
file/play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
file/prev.png
Normal file
BIN
file/prev.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
302
musicplayer.py
Normal file
302
musicplayer.py
Normal 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())
|
||||||
632
src/main.rs
632
src/main.rs
@ -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(())
|
|
||||||
}
|
|
||||||
62
test_file/林俊杰-光阴副本.lrc
Normal file
62
test_file/林俊杰-光阴副本.lrc
Normal 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] 鼓 DRUMS:Ash 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
|
||||||
BIN
test_file/林俊杰-光阴副本.wav
Normal file
BIN
test_file/林俊杰-光阴副本.wav
Normal file
Binary file not shown.
73
test_file/林俊杰-裂缝中的阳光.lrc
Normal file
73
test_file/林俊杰-裂缝中的阳光.lrc
Normal 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]阳光就逐渐洒满了其中
|
||||||
BIN
test_file/林俊杰-裂缝中的阳光.wav
Normal file
BIN
test_file/林俊杰-裂缝中的阳光.wav
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user