Compare commits

...

16 Commits

Author SHA1 Message Date
54c604ee9d chore: bump CI Qt version, update README.md 2024-09-27 23:21:57 +08:00
a2b9f94015 fix: last line of lyrics is incorrect 2024-09-26 00:03:26 +08:00
4b57dda342 chore: lrc now also support mm:ss.zzz instead of .zz only 2024-09-25 19:50:50 +08:00
04ed6d435c fix(CI): correct cmake prefix path for appveyor build 2024-09-23 20:45:38 +08:00
618a350e0d chore: add about dialog
to make it easier to know if taglib and/or libchardet are used.
2024-09-23 19:48:26 +08:00
b88ee1d0f1 fix: lyrics encoding and better lrc support 2024-09-22 17:02:40 +08:00
2a92f4ea7f feat: basic lyrics support 2024-09-20 20:59:40 +08:00
f4374a0768 chore: add aac to auto load list 2024-09-15 10:38:46 +08:00
b01dfe17fd fixes: follow system default audio output device...
...and also:

- fix crash when triggered the open file dialog to select music files
  but clicked cancel.
- support gif and jp*e*g suffix for drag and drop for skin selection
2024-09-12 21:36:54 +08:00
25eed8066b feat(UI): add skin support 2024-09-10 08:13:03 +08:00
8ac558ebc6 chore: RC file for windows, taglib tweaks 2024-09-08 15:36:28 +08:00
a910e85d97 chore: license cleanup, other tweaks 2024-09-08 00:34:45 +08:00
d28108f2e5 fix: don't use currentIndexChanged to load song 2024-09-07 12:14:54 +08:00
413711d073 chore(CI): github action msvc windows build 2024-09-07 00:57:40 +08:00
34d3989e9e refactor: reuse playlistmanager from my other project 2024-09-06 00:17:39 +08:00
86a5e4f722 fix(CI): cinst -> choco install, bump Qt version 2024-07-08 20:04:59 +08:00
36 changed files with 1392 additions and 2916 deletions

62
.github/workflows/windows.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Windows CI
on: [push, pull_request, workflow_dispatch]
jobs:
msvc-cmake-build:
strategy:
matrix:
vs: ['2022']
msvc_arch: ['x64']
qt_ver: ['6.7.3']
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
arch: 'win64_msvc2019_64'
version: ${{ matrix.qt_ver }}
modules: 'qtmultimedia'
- name: Build
shell: cmd
run: |
:: ------ env ------
set PWD=%cd%
set VS=${{ matrix.vs }}
set VCVARS="C:\Program Files (x86)\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat"
if not exist %VCVARS% set VCVARS="C:\Program Files\Microsoft Visual Studio\%VS%\Enterprise\VC\Auxiliary\Build\vcvarsall.bat"
call %VCVARS% ${{ matrix.msvc_arch }}
:: ------ dep ------
set CMAKE_PREFIX_PATH=%PWD%/dependencies_bin
mkdir dependencies_src
:: ===== uchardet =====
echo ::group::build uchardet
git clone -q https://gitlab.freedesktop.org/BLumia/uchardet.git --branch msvc dependencies_src/uchardet
cmake .\dependencies_src\uchardet -Bbuild_dependencies/uchardet -DBUILD_BINARY=OFF -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake --build build_dependencies/uchardet --config Release --target=install -j || goto :error
echo ::endgroup::
:: ===== pkg-config =====
choco install pkgconfiglite
set PKG_CONFIG_PATH=%PWD%/dependencies_bin/lib/pkgconfig
:: ===== taglib =====
git clone --recurse-submodules -q https://github.com/taglib/taglib.git dependencies_src/taglib
cmake .\dependencies_src\taglib -Bbuild_dependencies/taglib -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake --build build_dependencies/taglib --config Release --target=install -j || goto :error
:: ------ app ------
cmake -Bbuild . -DCMAKE_INSTALL_PREFIX="%PWD%\build\" || goto :error
cmake --build build --config Release -j || goto :error
cmake --build build --config Release --target=install
:: ------ pkg ------
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --skip-plugin-types tls,networkinformation build\bin\pmusic.exe
robocopy ./dependencies_bin/bin build/bin *.dll
if ErrorLevel 8 (exit /B 1)
copy LICENSE build/bin/
exit /B 0
- uses: actions/upload-artifact@v4
with:
name: "windows-msvc${{ matrix.vs }}-qt${{ matrix.qt_ver }}-cmake-package"
path: build/bin/*

81
.gitignore vendored
View File

@ -1,77 +1,8 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
# Common build folder
[Bb]uild/
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# IDE folder
.vscode/
# qtcreator generated files
*.pro.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe
# User Config
# -----------
*.user
*.user.*
# User config file
CMakeLists.txt.user*

View File

@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.12)
project(pineapple-music LANGUAGES CXX)
include (GNUInstallDirs)
include (FeatureSummary)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@ -13,7 +14,8 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 6.5.1 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
find_package(Qt6 6.6 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
find_package(uchardet)
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
@ -24,22 +26,19 @@ set (PMUSIC_CPP_FILES
main.cpp
mainwindow.cpp
seekableslider.cpp
playlistmodel.cpp
playlistmanager.cpp
singleapplicationmanager.cpp
qt/qplaylistfileparser.cpp
qt/qmediaplaylist.cpp
lrcbar.cpp
lyricsmanager.cpp
)
set (PMUSIC_HEADER_FILES
mainwindow.h
seekableslider.h
playlistmodel.h
playlistmanager.h
singleapplicationmanager.h
qt/qplaylistfileparser_p.h
qt/qmediaplaylist.h
qt/qmediaplaylist_p.h
lrcbar.h
lyricsmanager.h
)
set (PMUSIC_UI_FILES
@ -52,40 +51,36 @@ set (EXE_NAME pmusic)
file (GLOB PMUSIC_TS_FILES languages/*.ts)
set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES})
qt_create_translation(PMUSIC_QM_FILES ${PMUSIC_CPP_FILES_FOR_I18N} ${PMUSIC_TS_FILES})
add_executable(${EXE_NAME}
add_executable(${EXE_NAME} WIN32
${PMUSIC_HEADER_FILES}
${PMUSIC_CPP_FILES}
${PMUSIC_UI_FILES}
resources.qrc
# 3rd party code
FlacPic.h
ID3v2Pic.h
${PMUSIC_QM_FILES}
)
qt_add_translations(${EXE_NAME}
TS_FILES
${PMUSIC_TS_FILES}
)
if (WIN32)
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
endif ()
if (NOT TagLib_FOUND)
message (WARNING "TagLib not found!")
target_compile_definitions(${EXE_NAME} PRIVATE
NO_TAGLIB=1
)
target_compile_definitions(${EXE_NAME} PRIVATE NO_TAGLIB=1)
else ()
target_link_libraries(${EXE_NAME} PRIVATE PkgConfig::TagLib)
endif ()
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
# Extra build settings
if (WIN32)
set_property (
TARGET ${EXE_NAME}
PROPERTY WIN32_EXECUTABLE true
)
if (NOT uchardet_FOUND)
target_compile_definitions(${EXE_NAME} PRIVATE NO_UCHARDET=1)
else ()
target_link_libraries (${EXE_NAME} PRIVATE uchardet::libuchardet)
endif ()
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
# Install settings
if (WIN32)
# FIXME: try to avoid install to a "bin" subfolder under windows...
@ -96,15 +91,15 @@ elseif (UNIX)
endif ()
# install icon
install (
install(
FILES icons/app-icon.svg
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps"
RENAME pineapple-music.svg
RENAME net.blumia.pineapple-music.svg
)
# install shortcut
install (
FILES pineapple-music.desktop
install(
FILES dist/net.blumia.pineapple-music.desktop
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications"
)
endif()
@ -120,16 +115,4 @@ install (
${INSTALL_TARGETS_DEFAULT_ARGS}
)
if (WIN32)
set (QM_FILE_INSTALL_DIR "${CMAKE_INSTALL_BINDIR}/translations")
else ()
set (QM_FILE_INSTALL_DIR "${CMAKE_INSTALL_FULL_DATADIR}/pineapple-music/translations")
target_compile_definitions(${EXE_NAME}
PRIVATE QM_FILE_INSTALL_DIR=${QM_FILE_INSTALL_DIR}
)
endif ()
install (
FILES ${PMUSIC_QM_FILES}
DESTINATION ${QM_FILE_INSTALL_DIR}
)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)

263
FlacPic.h
View File

@ -1,263 +0,0 @@
/*
FLAC标签图片提取库 Ver 1.0
从FLAC文件中稳定、快捷、高效、便捷地提取出图片数据
支持BMP、JPEG、PNG、GIF图片格式
可将图片数据提取到文件或内存中,并能安全地释放内存
使用方式与ID3v2版本相同
ShadowPower 于2014/8/1 夜间
*/
#ifndef _ShadowPower_FLACPIC___
#define _ShadowPower_FLACPIC___
#define _CRT_SECURE_NO_WARNINGS
#ifndef NULL
#define NULL 0
#endif
#include <cstdio>
#include <cstdlib>
#include <memory.h>
#include <cstring>
typedef unsigned char byte;
namespace spFLAC {
//Flac元数据块头部结构体定义
struct FlacMetadataBlockHeader
{
byte flag; //标志位高1位是否为最后一个数据块低7位数据块类型
byte length[3]; //数据块长度,不含数据块头部
};
byte *pPicData = 0; //指向图片数据的指针
int picLength = 0; //存放图片数据长度
char picFormat[4] = {}; //存放图片数据的格式(扩展名)
//检测图片格式参数1数据返回值是否成功不是图片则失败
bool verificationPictureFormat(char *data)
{
//支持格式JPEG/PNG/BMP/GIF
byte jpeg[2] = { 0xff, 0xd8 };
byte png[8] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
byte gif[6] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
byte gif2[6] = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 };
byte bmp[2] = { 0x42, 0x4d };
memset(&picFormat, 0, 4);
if (memcmp(data, &jpeg, 2) == 0)
{
strcpy(picFormat, "jpg");
}
else if (memcmp(data, &png, 8) == 0)
{
strcpy(picFormat, "png");
}
else if (memcmp(data, &gif, 6) == 0 || memcmp(data, &gif2, 6) == 0)
{
strcpy(picFormat, "gif");
}
else if (memcmp(data, &bmp, 2) == 0)
{
strcpy(picFormat, "bmp");
}
else
{
return false;
}
return true;
}
//安全释放内存
void freePictureData()
{
if (pPicData)
{
delete pPicData;
}
pPicData = 0;
picLength = 0;
memset(&picFormat, 0, 4);
}
//将图片提取到内存参数1文件路径成功返回true
bool loadPictureData(const char *inFilePath)
{
freePictureData();
FILE *fp = NULL;
fp = fopen(inFilePath, "rb");
if (!fp) //如果打开失败
{
fp = NULL;
return false;
}
fseek(fp, 0, SEEK_SET); //设文件流指针到文件头部
byte magic[4] = {}; //存放校验数据
memset(&magic, 0, 4);
fread(&magic, 4, 1, fp); //读入校验数据
byte fLaC[4] = { 0x66, 0x4c, 0x61, 0x43 };
if (memcmp(&magic, &fLaC, 4) == 0)
{
//数据校验正确文件类型为Flac
FlacMetadataBlockHeader fmbh; //创建Flac元数据块头部结构体
memset(&fmbh, 0, 4); //清空内存
fread(&fmbh, 4, 1, fp); //读入头部数据
//计算数据块长度,不含头部
int blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];
int loopCount = 0; //循环计数,防死
while ((fmbh.flag & 0x7f) != 6)
{
//如果数据类型不是图片,此处循环执行
loopCount++;
if (loopCount > 40)
{
//循环40次没有遇到末尾就直接停止
fclose(fp);
fp = NULL;
return false; //可能文件不正常
}
fseek(fp, blockLength, SEEK_CUR); //跳过数据块
if ((fmbh.flag & 0x80) == 0x80)
{
//已经是最后一个数据块了,仍然不是图片
fclose(fp);
fp = NULL;
return false; //没有找到图片数据
}
//取得下一数据块头部
memset(&fmbh, 0, 4); //清空内存
fread(&fmbh, 4, 1, fp); //读入头部数据
blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];//计算数据块长度
}
//此时已到图片数据块
int nonPicDataLength = 0; //非图片数据长度
fseek(fp, 4, SEEK_CUR); //信仰之跃
nonPicDataLength += 4;
char nextJumpLength[4]; //下次要跳的长度
fread(&nextJumpLength, 4, 1, fp); //读取安全跳跃距离
nonPicDataLength += 4;
int jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];//计算数据块长度
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump!!
nonPicDataLength += jumpLength;
fread(&nextJumpLength, 4, 1, fp);
nonPicDataLength += 4;
jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump too!!
nonPicDataLength += jumpLength;
fseek(fp, 20, SEEK_CUR); //信仰之跃
nonPicDataLength += 20;
//非主流情况检测+获得文件格式
char tempData[20] = {};
memset(tempData, 0, 20);
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR); //回到原位
//判断40次一位一位跳到文件头
bool ok = false; //是否正确识别出文件头
for (int i = 0; i < 40; i++)
{
//校验文件头
if (verificationPictureFormat(tempData))
{
ok = true;
break;
}
else
{
//如果校验失败尝试继续向后校验
fseek(fp, 1, SEEK_CUR);
nonPicDataLength++;
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR);
}
}
if (!ok)
{
fclose(fp);
fp = NULL;
freePictureData();
return false; //无法识别的数据
}
//-----抵达图片数据区-----
picLength = blockLength - nonPicDataLength; //计算图片数据长度
pPicData = new byte[picLength]; //动态分配图片数据内存空间
memset(pPicData, 0, picLength); //清空图片数据内存
fread(pPicData, picLength, 1, fp); //得到图片数据
//------------------------
fclose(fp); //操作已完成,关闭文件。
}
else
{
//校验失败不是Flac
fclose(fp);
fp = NULL;
freePictureData();
return false;
}
return true;
}
//取得图片数据的长度
int getPictureLength()
{
return picLength;
}
//取得指向图片数据的指针
byte *getPictureDataPtr()
{
return pPicData;
}
//取得图片数据的扩展名(指针)
char *getPictureFormat()
{
return picFormat;
}
bool writePictureDataToFile(const char *outFilePath)
{
FILE *fp = NULL;
if (picLength > 0)
{
fp = fopen(outFilePath, "wb"); //打开目标文件
if (fp) //打开成功
{
fwrite(pPicData, picLength, 1, fp); //写入文件
fclose(fp); //关闭
return true;
}
else
{
return false; //文件打开失败
}
}
else
{
return false; //没有图像数据
}
}
//提取图片文件参数1输入文件参数2输出文件返回值是否成功
bool extractPicture(const char *inFilePath, const char *outFilePath)
{
if (loadPictureData(inFilePath)) //如果取得图片数据成功
{
if (writePictureDataToFile(outFilePath))
{
return true; //文件写出成功
}
else
{
return false; //文件写出失败
}
}
else
{
return false; //无图片数据
}
freePictureData();
}
}
#endif

View File

@ -1,441 +0,0 @@
/*
ID3v2标签图片提取库 Ver 1.0
支持ID3v2所有版本
从ID3v2标签中稳定、快捷、高效、便捷地提取出图片数据
支持BMP、JPEG、PNG、GIF图片格式
可将图片数据提取到文件或内存中,并能安全地释放内存
ShadowPower 于2014/8/1 上午
*/
#ifndef _ShadowPower_ID3V2PIC___
#define _ShadowPower_ID3V2PIC___
#define _CRT_SECURE_NO_WARNINGS
#ifndef NULL
#define NULL 0
#endif
#include <cstdio>
#include <cstdlib>
#include <memory.h>
#include <cstring>
typedef unsigned char byte;
namespace spID3 {
//ID3v2标签头部结构体定义
struct ID3V2Header
{
char identi[3];//ID3头部校验必须为“ID3”否则认为不存在ID3标签
byte major; //ID3版本号3是ID3v2.34是ID3v2.4,以此类推
byte revsion; //ID3副版本号此版本为00
byte flags; //标志位
byte size[4]; //标签大小不含标签头的10个字节
};
//ID3v2标签帧头部结构体定义
struct ID3V2FrameHeader
{
char FrameId[4];//标识符,用于描述此标签帧的内容类型
byte size[4]; //标签帧的大小不含标签头的10个字节
byte flags[2]; //标志位
};
struct ID3V22FrameHeader
{
char FrameId[3];//标识符,用于描述此标签帧的内容类型
byte size[3]; //标签帧的大小不含标签头的6个字节
};
byte *pPicData = 0; //指向图片数据的指针
int picLength = 0; //存放图片数据长度
char picFormat[4] = {}; //存放图片数据的格式(扩展名)
// ID3V2.3 & ID3V2.4 帧长度获取
inline int _frameLength34(ID3V2FrameHeader* fh, byte majorVersion) {
if (!fh || majorVersion < 3) {
return 0;
}
if (majorVersion == 3) {
return fh->size[0] * 0x1000000 + fh->size[1] * 0x10000 + fh->size[2] * 0x100 + fh->size[3];
}
return (fh->size[0] & 0x7f) * 0x200000 + (fh->size[1] & 0x7f) * 0x4000 + (fh->size[2] & 0x7f) * 0x80 + (fh->size[3] & 0x7f);
}
//检测图片格式参数1数据返回值是否成功不是图片则失败
bool verificationPictureFormat(char *data)
{
//支持格式JPEG/PNG/BMP/GIF
byte jpeg[2] = { 0xff, 0xd8 };
byte png[8] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
byte gif[6] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
byte gif2[6] = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 };
byte bmp[2] = { 0x42, 0x4d };
memset(&picFormat, 0, 4);
if (memcmp(data, &jpeg, 2) == 0)
{
strcpy(picFormat, "jpg");
}
else if (memcmp(data, &png, 8) == 0)
{
strcpy(picFormat, "png");
}
else if (memcmp(data, &gif, 6) == 0 || memcmp(data, &gif2, 6) == 0)
{
strcpy(picFormat, "gif");
}
else if (memcmp(data, &bmp, 2) == 0)
{
strcpy(picFormat, "bmp");
}
else
{
return false;
}
return true;
}
//安全释放内存
void freePictureData()
{
if (pPicData)
{
delete pPicData;
}
pPicData = 0;
picLength = 0;
memset(&picFormat, 0, 4);
}
//将图片提取到内存参数1文件路径成功返回true
bool loadPictureData(const char *inFilePath)
{
freePictureData();
FILE *fp = NULL; //初始化文件指针,置空
fp = fopen(inFilePath, "rb"); //以只读&二进制方式打开文件
if (!fp) //如果打开失败
{
fp = NULL;
return false;
}
fseek(fp, 0, SEEK_SET); //设文件流指针到文件头部(印象中打开之后默认在尾部)
//读取
ID3V2Header id3v2h; //创建一个ID3v2标签头结构体
memset(&id3v2h, 0, 10); //内存填010个字节
fread(&id3v2h, 10, 1, fp); //把文件头部10个字节写入结构体内存
//文件头识别
if (strncmp(id3v2h.identi, "ID3", 3) != 0)
{
fclose(fp);
fp = NULL;
return false;//没有ID3标签
}
//能运行到这里应该已经成功打开文件了
//计算整个标签长度每个字节仅7位有效
int tagTotalLength = (id3v2h.size[0] & 0x7f) * 0x200000 + (id3v2h.size[1] & 0x7f) * 0x4000 + (id3v2h.size[2] & 0x7f) * 0x80 + (id3v2h.size[3] & 0x7f);
if (id3v2h.major == 3 || id3v2h.major == 4) //ID3v2.3 或 ID3v2.4
{
ID3V2FrameHeader id3v2fh; //创建一个ID3v2标签帧头结构体
memset(&id3v2fh, 0, 10);
bool hasExtendedHeader = ((id3v2h.flags >> 6 & 0x1) == 1);//是否有扩展头
if (hasExtendedHeader)
{
//如果有扩展头
byte extendedHeaderSize[4] = {};
memset(&extendedHeaderSize, 0, 4);
fread(&extendedHeaderSize, 4, 1, fp);
//取得扩展头大小(不含以上数据)
int extendedHeaderLength = extendedHeaderSize[0] * 0x1000000 + extendedHeaderSize[1] * 0x10000 + extendedHeaderSize[2] * 0x100 + extendedHeaderSize[3];
//跳过扩展头
fseek(fp, extendedHeaderLength, SEEK_CUR);
}
fread(&id3v2fh, 10, 1, fp); //将数据写到ID3V2FrameHeader结构体中
int curDataLength = 10; //存放当前已经读取的数据大小刚才已经读入10字节
while ((strncmp(id3v2fh.FrameId, "APIC", 4) != 0))//如果帧头没有APIC标识符则循环执行
{
if (curDataLength > tagTotalLength)
{
fclose(fp);
fp = NULL;
return false; //未发现图片数据
}
//计算帧数据长度
//使用int不溢出的上限约2GB标签帧没有这么大吧……
int frameLength = _frameLength34(&id3v2fh, id3v2h.major);
fseek(fp, frameLength, SEEK_CUR); //向前跳跃到下一个帧头
memset(&id3v2fh, 0, 10); //清除帧头结构体数据
fread(&id3v2fh, 10, 1, fp); //重新读取数据
curDataLength += frameLength + 10; //记录当前所在的ID3标签位置以便退出循环
}
//计算一下当前图片帧的数据长度
int frameLength = _frameLength34(&id3v2fh, id3v2h.major);
/*
这是ID3v2.3图片帧的结构:
<Header for 'Attached picture', ID: "APIC">
头部10个字节的帧头
Text encoding $xx
要跳过一个字节(文字编码)
MIME type <text string> $00
跳过(文本 + /0这里可得到文件格式
Picture type $xx
跳过一个字节(图片类型)
Description <text string according to encoding> $00 (00)
跳过(文本 + /0这里可得到描述信息
Picture data <binary data>
这是真正的图片数据
*/
int nonPicDataLength = 0; //非图片数据的长度
fseek(fp, 1, SEEK_CUR); //信仰之跃
nonPicDataLength++;
char tempData[20] = {}; //临时存放数据的空间
char mimeType[20] = {}; //图片类型
int mimeTypeLength = 0; //图片类型文本长度
fread(&tempData, 20, 1, fp);//取得一小段数据
fseek(fp, -20, SEEK_CUR); //回到原位
strcpy(mimeType, tempData); //复制出一个字符串
mimeTypeLength = strlen(mimeType) + 1; //测试字符串长度补上末尾00
fseek(fp, mimeTypeLength, SEEK_CUR); //跳到此数据之后
nonPicDataLength += mimeTypeLength; //记录长度
fseek(fp, 1, SEEK_CUR); //再一次信仰之跃
nonPicDataLength++;
int temp = 0; //记录当前字节数据的变量
fread(&temp, 1, 1, fp); //读取一个字节
nonPicDataLength++; //+1
while (temp) //循环到temp为0
{
fread(&temp, 1, 1, fp); //如果不是0继续读一字节的数据
nonPicDataLength++; //计数
}
//跳过了Description文本以及末尾的\0
//非主流情况检测+获得文件格式
memset(tempData, 0, 20);
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR); //回到原位
//判断40次一位一位跳到文件头
bool ok = false; //是否正确识别出文件头
for (int i = 0; i < 40; i++)
{
//校验文件头
if (verificationPictureFormat(tempData))
{
ok = true;
break;
}
else
{
//如果校验失败尝试继续向后校验
fseek(fp, 1, SEEK_CUR);
nonPicDataLength++;
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR);
}
}
if (!ok)
{
fclose(fp);
fp = NULL;
freePictureData();
return false; //无法识别的数据
}
//-----真正的图片数据-----
picLength = frameLength - nonPicDataLength; //计算图片数据长度
pPicData = new byte[picLength]; //动态分配图片数据内存空间
memset(pPicData, 0, picLength); //清空图片数据内存
fread(pPicData, picLength, 1, fp); //得到图片数据
//------------------------
fclose(fp); //操作已完成,关闭文件。
}
else if (id3v2h.major == 2)
{
//ID3v2.2
ID3V22FrameHeader id3v2fh; //创建一个ID3v2.2标签帧头结构体
memset(&id3v2fh, 0, 6);
fread(&id3v2fh, 6, 1, fp); //将数据写到ID3V2.2FrameHeader结构体中
int curDataLength = 6; //存放当前已经读取的数据大小刚才已经读入6字节
while ((strncmp(id3v2fh.FrameId, "PIC", 3) != 0))//如果帧头没有PIC标识符则循环执行
{
if (curDataLength > tagTotalLength)
{
fclose(fp);
fp = NULL;
return false; //未发现图片数据
}
//计算帧数据长度
int frameLength = id3v2fh.size[0] * 0x10000 + id3v2fh.size[1] * 0x100 + id3v2fh.size[2];
fseek(fp, frameLength, SEEK_CUR); //向前跳跃到下一个帧头
memset(&id3v2fh, 0, 6); //清除帧头结构体数据
fread(&id3v2fh, 6, 1, fp); //重新读取数据
curDataLength += frameLength + 6; //记录当前所在的ID3标签位置以便退出循环
}
int frameLength = id3v2fh.size[0] * 0x10000 + id3v2fh.size[1] * 0x100 + id3v2fh.size[2]; //如果读到了图片帧,计算帧长
/*
数据格式:
Attached picture "PIC"
Frame size $xx xx xx
Text encoding $xx
Image format $xx xx xx
Picture type $xx
Description <textstring> $00 (00)
Picture data <binary data>
*/
int nonPicDataLength = 0; //非图片数据的长度
fseek(fp, 1, SEEK_CUR); //信仰之跃 Text encoding
nonPicDataLength++;
char imageType[4] = {};
memset(&imageType, 0, 4);
fread(&imageType, 3, 1, fp);//图像格式
nonPicDataLength += 3;
fseek(fp, 1, SEEK_CUR); //信仰之跃 Picture type
nonPicDataLength++;
int temp = 0; //记录当前字节数据的变量
fread(&temp, 1, 1, fp); //读取一个字节
nonPicDataLength++; //+1
while (temp) //循环到temp为0
{
fread(&temp, 1, 1, fp); //如果不是0继续读一字节的数据
nonPicDataLength++; //计数
}
//跳过了Description文本以及末尾的\0
//非主流情况检测
char tempData[20] = {};
memset(tempData, 0, 20);
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR); //回到原位
//判断40次一位一位跳到文件头
bool ok = false; //是否正确识别出文件头
for (int i = 0; i < 40; i++)
{
//校验文件头
if (verificationPictureFormat(tempData))
{
ok = true;
break;
}
else
{
//如果校验失败尝试继续向后校验
fseek(fp, 1, SEEK_CUR);
nonPicDataLength++;
fread(&tempData, 8, 1, fp);
fseek(fp, -8, SEEK_CUR);
}
}
if (!ok)
{
fclose(fp);
fp = NULL;
freePictureData();
return false; //无法识别的数据
}
//-----真正的图片数据-----
picLength = frameLength - nonPicDataLength; //计算图片数据长度
pPicData = new byte[picLength]; //动态分配图片数据内存空间
memset(pPicData, 0, picLength); //清空图片数据内存
fread(pPicData, picLength, 1, fp); //得到图片数据
//------------------------
fclose(fp); //操作已完成,关闭文件。
}
else
{
//其余不支持的版本
fclose(fp);//关闭
fp = NULL;
return false;
}
return true;
}
//取得图片数据的长度
int getPictureLength()
{
return picLength;
}
//取得指向图片数据的指针
byte *getPictureDataPtr()
{
return pPicData;
}
//取得图片数据的扩展名(指针)
char *getPictureFormat()
{
return picFormat;
}
bool writePictureDataToFile(const char *outFilePath)
{
FILE *fp = NULL;
if (picLength > 0)
{
fp = fopen(outFilePath, "wb"); //打开目标文件
if (fp) //打开成功
{
fwrite(pPicData, picLength, 1, fp); //写入文件
fclose(fp); //关闭
return true;
}
else
{
return false; //文件打开失败
}
}
else
{
return false; //没有图像数据
}
}
//提取图片文件参数1输入文件参数2输出文件返回值是否成功
bool extractPicture(const char *inFilePath, const char *outFilePath)
{
if (loadPictureData(inFilePath)) //如果取得图片数据成功
{
if (writePictureDataToFile(outFilePath))
{
return true; //文件写出成功
}
else
{
return false; //文件写出失败
}
}
else
{
return false; //无图片数据
}
freePictureData();
}
}
#endif

View File

@ -1,7 +1,18 @@
_**This is a not ready to use, toy project**_
## Read Before Use
Since **I** just need a simple player which *just works* right now, so I did many things in a dirty way. Don't feel so weird if you saw something I did in this project is using a bad approach.
### Feature Notice
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
- No music library management support and there won't be one!
- It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
- Limited lyrics (`.lrc`) loading support:
- Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
- Multi-line lyrics and duplicated timestamps are not supported
- Extensions (Walaoke and A2 extension) are not supported
## Build
Current state, we need:
@ -13,37 +24,19 @@ Current state, we need:
Then we can build it with any proper c++ compiler like g++ or msvc.
### Linux
Building it just requires normal cmake building steps:
Just normal build process as other program. Nothing special ;)
### Windows
Install the depts manually is a nightmare. I use [KDE Craft](https://community.kde.org/Craft) but MSYS2 should also works. FYI currently this project is not intended to works under Windows (it should works and I also did some simple test though).
### macOS
I don't have a mac, so no support at all.
```shell
$ cmake -Bbuild
$ cmake --build build
```
## Help Translation!
[Translate this project on Transifex!](https://www.transifex.com/blumia/pineapple-music/)
Feel free to open up an issue to request an new language to translate.
[Translate this project on Codeberg's Weblate!](https://translate.codeberg.org/projects/pineapple-apps/pineapple-music/)
## About License
Since this is a toy repo, I don't spend much time about the license stuff. Currently this project use some assets and code from [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer), which have a very interesting license -- do whatever you want but cannot be used as homework -- obviously it's not a so called *free* license. I *may* do some license housecleaning works by replaceing the assets and code implementation when the code become reasonable, and the final codebase may probably released under MIT license.
Pineapple Music as a whole is licensed under MIT license. Individual files may have a different, but compatible license.
Anyway here is a list of file which is in non-free state (with license: do whatever you want but cannot be used as homework):
- All png images inside `icons` folder.
- seekableslider.{h,cpp}
And something from ShadowPlayer but in other license:
- {Flac,ID3v2}Pic.h : [AlbumCoverExtractor](https://github.com/ShadowPower/AlbumCoverExtractor), with [MIT License](https://github.com/ShadowPower/AlbumCoverExtractor/blob/master/LICENSE)
Also there are some source code which I copy-paste from Qt codebase, which released under BSD-3-Clause license by the Qt Company:
- playlistmodel.{h,cpp}
All *png* images inside `icons` folder are originally created by [@ShadowPower](https://github.com/ShadowPower/) for [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer). These images are licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) license, grant by the original author.

View File

@ -5,8 +5,8 @@ environment:
PACKAGE_INSTALL_ROOT: C:\projects\pir
PKG_CONFIG_PATH: C:\projects\pir\lib\pkgconfig
matrix:
- build_name: mingw1120_64_qt6_5
QTPATH: C:\Qt\6.5\mingw_64
- build_name: mingw1120_64_qt6_7
QTPATH: C:\Qt\6.7\mingw_64
MINGW64: C:\Qt\Tools\mingw1120_64
install:
@ -21,21 +21,26 @@ install:
build_script:
# prepare
- mkdir 3rdparty
- cinst ninja
- cinst pkgconfiglite
# build taglib
- choco install ninja
- choco install pkgconfiglite
- cd 3rdparty
- git clone -q https://github.com/taglib/taglib.git
# build uchardet
- git clone -q https://gitlab.freedesktop.org/uchardet/uchardet.git
- cd uchardet
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_BINARY=OFF
- cmake --build . --target install
- cd %APPVEYOR_BUILD_FOLDER%
# build taglib
- git clone --recurse-submodules -q https://github.com/taglib/taglib.git
- cd taglib
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON
- cmake --build .
- cmake --build . --target install
- cd %APPVEYOR_BUILD_FOLDER%
- tree %PACKAGE_INSTALL_ROOT% /f
# finally...
- mkdir build
- cd build
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX='%cd%'
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%PACKAGE_INSTALL_ROOT% -DCMAKE_INSTALL_PREFIX='%cd%'
- cmake --build .
- cmake --build . --target install
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...

BIN
assets/icons/app-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

17
assets/pineapple-music.rc Normal file
View File

@ -0,0 +1,17 @@
IDI_ICON1 ICON DISCARDABLE "icons/app-icon.ico"
1 VERSIONINFO
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "FileDescription", "Pineapple Music - Music Player"
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2024 Gary Wang"
VALUE "ProductName", "Pineapple Music"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View File

@ -3,7 +3,7 @@ Categories=Audio;Player;
Comment=Pineapple Music Audio Player.
Exec=pmusic %F
GenericName=Music
Icon=pineapple-music
Icon=net.blumia.pineapple-music
Keywords=Picture;Image;Viewer;Jpg;Jpeg;Png;
MimeType=application/ogg;application/x-ogg;audio/ogg;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/ogg;video/x-ogm;video/x-ogm+ogg;video/x-theora+ogg;video/x-theora;audio/x-speex;audio/opus;application/x-flac;audio/flac;audio/x-flac;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-msvideo;audio/x-pn-windows-acm;video/divx;video/msvideo;video/vnd.divx;video/avi;video/x-avi;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/vnd.rn-realaudio;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-real-audio;audio/x-realaudio;video/vnd.rn-realvideo;audio/mpeg;audio/mpg;audio/mp1;audio/mp2;audio/mp3;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpg;video/mp2t;video/mpeg;video/mpeg-system;video/x-mpeg;video/x-mpeg2;video/x-mpeg-system;application/mpeg4-iod;application/mpeg4-muxcodetable;application/x-extension-m4a;application/x-extension-mp4;audio/aac;audio/m4a;audio/mp4;audio/x-m4a;audio/x-aac;video/mp4;video/mp4v-es;video/x-m4v;application/x-quicktime-media-link;application/x-quicktimeplayer;video/quicktime;application/x-matroska;audio/x-matroska;video/x-matroska;video/webm;audio/webm;audio/3gpp;audio/3gpp2;audio/AMR;audio/AMR-WB;video/3gp;video/3gpp;video/3gpp2;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtsp;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/icy;x-scheme-handler/icyx;application/x-cd-image;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player;application/ram;application/xspf+xml;audio/mpegurl;audio/x-mpegurl;audio/scpls;audio/x-scpls;text/google-video-pointer;text/x-google-video-pointer;video/vnd.mpegurl;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.ms-wpl;application/sdp;audio/dv;video/dv;audio/x-aiff;audio/x-pn-aiff;video/x-anim;video/x-nsv;video/fli;video/flv;video/x-flc;video/x-fli;video/x-flv;audio/wav;audio/x-pn-au;audio/x-pn-wav;audio/x-wav;audio/x-adpcm;audio/ac3;audio/eac3;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/basic;audio/midi;audio/x-ape;audio/x-gsm;audio/x-musepack;audio/x-tta;audio/x-wavpack;audio/x-shorten;application/x-shockwave-flash;application/x-flash-video;misc/ultravox;image/vnd.rn-realpix;audio/x-it;audio/x-mod;audio/x-s3m;audio/x-xm;application/mxf;
Name=Pineapple Music

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
icons/skin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -2,104 +2,85 @@
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>MainWindow</name>
<name>LrcBar</name>
<message>
<location filename="../mainwindow.cpp" line="106"/>
<source>Mono</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="108"/>
<source>Stereo</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="110"/>
<source>%1 Channels</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="238"/>
<source>Select songs to play</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="240"/>
<source>Audio Files</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="23"/>
<source>Pineapple Player</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="231"/>
<source>^</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="297"/>
<source>No song loaded...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="304"/>
<source>Drag and drop file to load</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="313"/>
<location filename="../mainwindow.ui" line="320"/>
<source>0:00</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="116"/>
<source>Sample Rate: %1 Hz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="121"/>
<source>Bitrate: %1 Kbps</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="126"/>
<source>Channel Count: %1</source>
<location filename="../lrcbar.cpp" line="88"/>
<source>(Interlude...)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QMediaPlaylist</name>
<name>MainWindow</name>
<message>
<location filename="../qt/qmediaplaylist.cpp" line="460"/>
<source>The file could not be accessed.</source>
<location filename="../mainwindow.cpp" line="93"/>
<source>Mono</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="301"/>
<source>%1 playlist type is unknown</source>
<location filename="../mainwindow.cpp" line="95"/>
<source>Stereo</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="362"/>
<source>invalid line in playlist file</source>
<location filename="../mainwindow.cpp" line="97"/>
<source>%1 Channels</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="485"/>
<source>Invalid stream</source>
<location filename="../mainwindow.cpp" line="241"/>
<source>Select songs to play</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="509"/>
<source>%1 does not exist</source>
<location filename="../mainwindow.cpp" line="243"/>
<source>Audio Files</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="557"/>
<source>Empty file provided</source>
<location filename="../mainwindow.cpp" line="576"/>
<source>Select image as background skin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="578"/>
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="23"/>
<location filename="../lrcbar.cpp" line="89"/>
<source>Pineapple Music</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="313"/>
<source>No song loaded...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="325"/>
<source>Drag and drop file to load</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="338"/>
<source>Lrc</source>
<comment>Lyrics</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="103"/>
<source>Sample Rate: %1 Hz</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="108"/>
<source>Bitrate: %1 Kbps</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="113"/>
<source>Channel Count: %1</source>
<translation type="unfinished"></translation>
</message>
</context>

View File

@ -1,114 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>LrcBar</name>
<message>
<location filename="../lrcbar.cpp" line="88"/>
<source>(Interlude...)</source>
<translation></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="106"/>
<location filename="../mainwindow.cpp" line="93"/>
<source>Mono</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="108"/>
<location filename="../mainwindow.cpp" line="95"/>
<source>Stereo</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="110"/>
<location filename="../mainwindow.cpp" line="97"/>
<source>%1 Channels</source>
<translation>%1 </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="238"/>
<location filename="../mainwindow.cpp" line="241"/>
<source>Select songs to play</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="240"/>
<location filename="../mainwindow.cpp" line="243"/>
<source>Audio Files</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Select image as background skin</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="578"/>
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
<translation> (*.jpg *.jpeg *.png *.gif)</translation>
</message>
<message>
<location filename="../mainwindow.ui" line="23"/>
<source>Pineapple Player</source>
<translation type="unfinished"></translation>
<location filename="../lrcbar.cpp" line="89"/>
<source>Pineapple Music</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="231"/>
<source>^</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="297"/>
<location filename="../mainwindow.ui" line="313"/>
<source>No song loaded...</source>
<translation>...</translation>
</message>
<message>
<location filename="../mainwindow.ui" line="304"/>
<location filename="../mainwindow.ui" line="325"/>
<source>Drag and drop file to load</source>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.ui" line="313"/>
<location filename="../mainwindow.ui" line="320"/>
<source>0:00</source>
<translation type="unfinished"></translation>
<location filename="../mainwindow.ui" line="338"/>
<source>Lrc</source>
<comment>Lyrics</comment>
<translation></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="116"/>
<location filename="../mainwindow.cpp" line="103"/>
<source>Sample Rate: %1 Hz</source>
<translation>: %1 Hz</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="121"/>
<location filename="../mainwindow.cpp" line="108"/>
<source>Bitrate: %1 Kbps</source>
<translation>: %1 Kbps</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="126"/>
<location filename="../mainwindow.cpp" line="113"/>
<source>Channel Count: %1</source>
<translation>: %1</translation>
</message>
</context>
<context>
<name>QMediaPlaylist</name>
<message>
<location filename="../qt/qmediaplaylist.cpp" line="460"/>
<source>The file could not be accessed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="301"/>
<source>%1 playlist type is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="362"/>
<source>invalid line in playlist file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="485"/>
<source>Invalid stream</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="509"/>
<source>%1 does not exist</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qt/qplaylistfileparser.cpp" line="557"/>
<source>Empty file provided</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../main.cpp" line="28"/>
<source>File list.</source>
<translation></translation>
<translation></translation>
</message>
</context>
</TS>

128
lrcbar.cpp Normal file
View File

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "lrcbar.h"
#include "lyricsmanager.h"
#include <QMouseEvent>
#include <QPainter>
#include <QWindow>
LrcBar::LrcBar(QWidget *parent)
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
, m_lrcMgr(new LyricsManager(this))
{
m_font.setPointSize(30);
m_font.setStyleStrategy(QFont::PreferAntialias);
QSize windowSize(sizeHint());
QFontMetrics fm(m_font);
m_baseLinearGradient.setColorAt(0, QColor(20, 100, 200));
m_baseLinearGradient.setColorAt(1, QColor(0, 80, 255));
m_baseLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
m_baseLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
m_maskLinearGradient.setColorAt(0, QColor(255, 128, 0));
m_maskLinearGradient.setColorAt(0.5, QColor(255, 255, 0));
m_maskLinearGradient.setColorAt(1, QColor(255, 128, 0));
m_maskLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
m_maskLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
windowSize));
}
LrcBar::~LrcBar()
{
}
bool LrcBar::loadLyrics(QString filepath)
{
m_currentTimeMs = 0;
return m_lrcMgr->loadLyrics(filepath);
}
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
{
if (!isVisible()) return;
m_currentTimeMs = timestampMs;
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
update();
}
QSize LrcBar::sizeHint() const
{
return QSize(1000, 88);
}
void LrcBar::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
window()->windowHandle()->startSystemMove();
event->accept();
}
return QWidget::mouseMoveEvent(event);
}
void LrcBar::paintEvent(QPaintEvent *)
{
QPainter painter(this);
if (underMouse()) {
painter.setBrush(QBrush(QColor(255, 255, 255, 120)));
painter.setPen(Qt::NoPen);
painter.drawRect(0, 0, width(), height());
}
painter.setFont(m_font);
painter.setRenderHint(QPainter::Antialiasing, true);
QString curLrc(m_lrcMgr->lyrics().trimmed());
if (curLrc.isEmpty()) {
curLrc = m_lrcMgr->hasLyrics() ? tr("(Interlude...)")
: QCoreApplication::translate("MainWindow", "Pineapple Music", nullptr);
}
QFontMetrics fm(m_font);
int lrcWidth = fm.horizontalAdvance(curLrc);
int maskWidth = lrcWidth * m_lrcMgr->maskPercent(m_currentTimeMs);
int startOffsetX = 0;
if (fm.horizontalAdvance(curLrc) < width()) {
startOffsetX = (width() - lrcWidth) / 2;
} else {
if (maskWidth < width() / 2) {
startOffsetX = 0;
} else if (lrcWidth - maskWidth < width() / 2) {
startOffsetX = width() - lrcWidth;
} else {
startOffsetX = 0 - (maskWidth - width() / 2);
}
}
// shadow
painter.setPen(QColor(0, 0, 0, 80));
painter.drawText(startOffsetX + 2, 2, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
// text itself
painter.setPen(QPen(m_baseLinearGradient, 0));
painter.drawText(startOffsetX, 0, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
// mask
painter.setPen(QPen(m_maskLinearGradient, 0));
painter.drawText(startOffsetX, 0, maskWidth, this->height(), Qt::AlignVCenter, curLrc);
}
void LrcBar::enterEvent(QEnterEvent *)
{
update();
}
void LrcBar::leaveEvent(QEvent *)
{
update();
}

36
lrcbar.h Normal file
View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QLinearGradient>
#include <QMediaPlayer>
#include <QWidget>
class LyricsManager;
class LrcBar : public QWidget
{
Q_OBJECT
public:
explicit LrcBar(QWidget *parent);
~LrcBar();
bool loadLyrics(QString filepath);
void playbackPositionChanged(int timestampMs, int totalTimeMs);
protected:
QSize sizeHint() const override;
void mouseMoveEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *) override;
void enterEvent(QEnterEvent *) override;
void leaveEvent(QEvent *) override;
private:
int m_currentTimeMs = 0;
LyricsManager * m_lrcMgr;
QFont m_font;
QLinearGradient m_baseLinearGradient;
QLinearGradient m_maskLinearGradient;
bool m_underMouse = false;
};

177
lyricsmanager.cpp Normal file
View File

@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "lyricsmanager.h"
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStringConverter>
#ifndef NO_UCHARDET
#include <uchardet/uchardet.h>
#endif
Q_LOGGING_CATEGORY(lcLyrics, "pmusic.lyrics")
Q_LOGGING_CATEGORY(lcLyricsParser, "pmusic.lyrics.parser")
LyricsManager::LyricsManager(QObject *parent)
: QObject(parent)
{
}
LyricsManager::~LyricsManager()
{
}
bool LyricsManager::loadLyrics(QString filepath)
{
// reset state
reset();
// check and load file
QFileInfo fileInfo(filepath);
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
}
if (!fileInfo.exists()) return false;
QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
QByteArray fileContent(file.readAll());
#ifndef NO_UCHARDET
uchardet_t handle = uchardet_new();
uchardet_handle_data(handle, fileContent.data(), fileContent.length());
uchardet_data_end(handle);
const char* encoding = uchardet_get_charset(handle);
qCDebug(lcLyrics) << "Detected encoding:" << (encoding == NULL ? "unknown" : encoding);
QStringList lines;
if (QStringConverter::availableCodecs().contains(QString(encoding))) {
auto toUtf16 = QStringDecoder(encoding);
QString decodedResult = toUtf16(fileContent);
lines = decodedResult.split('\n');
} else {
lines = QString(fileContent).split('\n');
}
uchardet_delete(handle);
#else
QStringList lines = QString(fileContent).split('\n');
#endif
file.close();
// parse lyrics timestamp
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2,3})\](.*))regex");
bool tagSectionPassed = false;
for (QString line : std::as_const(lines)) {
line = line.trimmed();
if (line.isEmpty()) continue;
if (!tagSectionPassed) {
QRegularExpressionMatch tagMatch = tagRegex.match(line);
if (tagMatch.hasMatch()) {
QString tag(tagMatch.captured(1));
if (tag == QLatin1String("offset")) {
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
m_timeOffset = -tagMatch.captured(2).toInt();
qCDebug(lcLyricsParser) << m_timeOffset;
}
qCDebug(lcLyricsParser) << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
continue;
}
}
QList<int> timestamps;
QString currentLrc;
QRegularExpressionMatch match = lrcRegex.match(line);
while (match.hasMatch()) {
tagSectionPassed = true;
QTime timestamp(QTime::fromString(match.captured(1), "m:s.zz"));
timestamps.append(timestamp.msecsSinceStartOfDay());
currentLrc = match.captured(2);
match = lrcRegex.match(currentLrc);
}
if (!timestamps.isEmpty()) {
for (int timestamp : std::as_const(timestamps)) {
m_lyricsMap.insert(timestamp, currentLrc);
qCDebug(lcLyricsParser) << "[lrc]" << timestamp << currentLrc;
}
}
}
if (!m_lyricsMap.isEmpty()) {
m_timestampList = m_lyricsMap.keys();
std::sort(m_timestampList.begin(), m_timestampList.end());
return true;
}
return false;
}
bool LyricsManager::hasLyrics() const
{
return !m_lyricsMap.isEmpty();
}
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
{
if (!hasLyrics()) return;
// TODO: we don't need to find from the top everytime the time is updated
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end(), [&curTimeMs, this](int timestamp) -> bool {
return (timestamp + m_timeOffset) > curTimeMs;
});
m_nextLyricsTime = iter == m_timestampList.end() ? totalTimeMs : *iter;
if (iter != m_timestampList.begin()) {
iter--;
}
m_currentLyricsTime = *iter;
}
QString LyricsManager::lyrics(int lineOffset) const
{
if (!hasLyrics()) return QString();
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
if (index >= 0 && index < m_timestampList.count()) {
int timestamp = m_timestampList.at(index);
return m_lyricsMap.value(timestamp);
} else {
return QString();
}
}
double LyricsManager::maskPercent(int curTimeMs)
{
if (!hasLyrics()) return 0;
if (curTimeMs <= currentLyricsTime()) return 0;
if (curTimeMs >= nextLyricsTime()) return 1;
if (m_nextLyricsTime == currentLyricsTime()) return 1;
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
}
void LyricsManager::reset()
{
m_currentLyricsTime = 0;
m_nextLyricsTime = 0;
m_timeOffset = 0;
m_lyricsMap.clear();
m_timestampList.clear();
}
int LyricsManager::currentLyricsTime() const
{
return m_currentLyricsTime + m_timeOffset;
}
int LyricsManager::nextLyricsTime() const
{
return m_nextLyricsTime + m_timeOffset;
}

41
lyricsmanager.h Normal file
View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QHash>
#include <QList>
#include <QLoggingCategory>
#include <QObject>
Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
class LyricsManager : public QObject
{
Q_OBJECT
public:
explicit LyricsManager(QObject *parent);
~LyricsManager();
bool loadLyrics(QString filepath);
bool hasLyrics() const;
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
QString lyrics(int lineOffset = 0) const;
double maskPercent(int curTimeMs);
protected:
private:
void reset();
int currentLyricsTime() const;
int nextLyricsTime() const;
QHash<int, QString> m_lyricsMap;
QList<int> m_timestampList;
int m_currentLyricsTime = 0;
int m_nextLyricsTime = 0;
int m_timeOffset = 0;
};

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "mainwindow.h"
#include "singleapplicationmanager.h"
@ -14,13 +18,9 @@ int main(int argc, char *argv[])
QApplication a(argc, argv);
QTranslator translator;
QString qmDir;
#ifdef _WIN32
qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations");
#else
qmDir = QT_STRINGIFY(QM_FILE_INSTALL_DIR);
#endif
translator.load(QString("pineapple-music_%1").arg(QLocale::system().name()), qmDir);
if (translator.load(QLocale(), QLatin1String("pineapple-music"), QLatin1String("_"), QLatin1String(":/i18n"))) {
a.installTranslator(&translator);
}
a.installTranslator(&translator);
// parse commandline arguments

View File

@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "playlistmodel.h"
#include "qt/qmediaplaylist.h"
#include "ID3v2Pic.h"
#include "FlacPic.h"
#include "playlistmanager.h"
#include "lrcbar.h"
// taglib
#ifndef NO_TAGLIB
@ -14,6 +15,7 @@
#include <QPainter>
#include <QMediaPlayer>
#include <QMediaMetaData>
#include <QAudioOutput>
#include <QPropertyAnimation>
#include <QFileDialog>
@ -24,21 +26,39 @@
#include <QCollator>
#include <QMimeData>
#include <QWindow>
#include <QStandardPaths>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QMessageBox>
#include <QStringBuilder>
constexpr QSize miniSize(490, 160);
constexpr QSize fullSize(490, 420);
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_mediaDevices(new QMediaDevices(this))
, m_mediaPlayer(new QMediaPlayer(this))
, m_audioOutput(new QAudioOutput(this))
, m_playlistModel(new PlaylistModel(this))
, m_lrcbar(new LrcBar(nullptr))
, m_playlistManager(new PlaylistManager(this))
{
ui->setupUi(this);
m_playlistManager->setAutoLoadFilterSuffixes({
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac"
});
m_mediaPlayer->setAudioOutput(m_audioOutput);
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
ui->playlistView->setModel(m_playlistManager->model());
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
this->setAttribute(Qt::WA_TranslucentBackground, true);
ui->actionHelp->setShortcut(QKeySequence::HelpContents);
addAction(ui->actionHelp);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
setAttribute(Qt::WA_TranslucentBackground, true);
loadSkinData();
initConnections();
initUiAndAnimation();
@ -47,6 +67,7 @@ MainWindow::MainWindow(QWidget *parent)
MainWindow::~MainWindow()
{
delete m_lrcbar;
delete ui;
}
@ -55,45 +76,12 @@ void MainWindow::commandlinePlayAudioFiles(QStringList audioFiles)
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
if (!audioFileUrls.isEmpty()) {
if (audioFileUrls.count() == 1) {
loadPlaylistBySingleLocalFile(audioFileUrls.first().toLocalFile());
} else {
createPlaylist(audioFileUrls);
}
m_mediaPlayer->play();
}
}
void MainWindow::loadPlaylistBySingleLocalFile(const QString &path)
{
QFileInfo info(path);
QDir dir(info.path());
QString currentFileName = info.fileName();
QStringList entryList = dir.entryList({"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga"},
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> urlList;
int currentFileIndex = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & oneEntry = entryList.at(i);
urlList.append(QUrl::fromLocalFile(dir.absoluteFilePath(oneEntry)));
if (oneEntry == currentFileName) {
currentFileIndex = i;
QModelIndex modelIndex = m_playlistManager->loadPlaylist(audioFileUrls);
if (modelIndex.isValid()) {
loadByModelIndex(modelIndex);
play();
}
}
if (currentFileIndex == -1) {
// not in the list probably because of the suffix is not a common one, add it to the first one anyway.
urlList.prepend(QUrl::fromLocalFile(path));
currentFileIndex = 0;
}
createPlaylist(urlList, currentFileIndex);
}
void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt)
@ -169,10 +157,14 @@ void MainWindow::paintEvent(QPaintEvent * e)
QPainter painter(this);
painter.setPen(Qt::NoPen);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
// Temp bg
painter.setBrush(QColor(20, 32, 83));
if (m_skin.isNull()) {
painter.setBrush(QColor(40, 50, 123));
painter.drawRect(0, 0, width(), height());
} else {
painter.drawPixmap(0, 0, m_skin);
}
painter.setBrush(QBrush(m_bgLinearGradient));
painter.drawRect(0, 0, width(), height());
@ -193,7 +185,6 @@ void MainWindow::mousePressEvent(QMouseEvent *event)
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton && m_clickedOnWindow) {
qDebug() << "??" << event << event->flags() << event->isBeginEvent() << event->isEndEvent();
window()->windowHandle()->startSystemMove();
event->accept();
}
@ -204,7 +195,6 @@ void MainWindow::mouseMoveEvent(QMouseEvent *event)
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
m_clickedOnWindow = false;
qDebug() << "?";
return QMainWindow::mouseReleaseEvent(event);
}
@ -227,56 +217,62 @@ void MainWindow::dropEvent(QDropEvent *e)
return;
}
// TODO: file/format filter?
if (fileName.endsWith(".png") || fileName.endsWith(".jpg") ||
fileName.endsWith(".jpeg") || fileName.endsWith(".gif")) {
setSkin(fileName, true);
return;
}
createPlaylist(urls);
m_mediaPlayer->play();
if (fileName.endsWith(".lrc")) {
m_lrcbar->loadLyrics(fileName);
return;
}
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
if (modelIndex.isValid()) {
loadByModelIndex(modelIndex);
play();
}
}
void MainWindow::loadFile()
{
QStringList musicFolders(QStandardPaths::standardLocations(QStandardPaths::MusicLocation));
musicFolders.append(QDir::homePath());
QStringList files = QFileDialog::getOpenFileNames(this,
tr("Select songs to play"),
QDir::homePath(),
musicFolders.first(),
tr("Audio Files") + " (*.mp3 *.wav *.aiff *.ape *.flac *.ogg *.oga)");
if (files.isEmpty()) return;
QList<QUrl> urlList;
for (const QString & fileName : files) {
urlList.append(QUrl::fromLocalFile(fileName));
}
createPlaylist(urlList);
m_playlistManager->loadPlaylist(urlList);
m_mediaPlayer->setSource(urlList.first());
m_lrcbar->loadLyrics(urlList.first().toLocalFile());
}
/*
* The returned QMediaPlaylist* ownership belongs to the internal QMediaPlayer instance.
*/
void MainWindow::createPlaylist(QList<QUrl> urlList, int index)
void MainWindow::loadByModelIndex(const QModelIndex & index)
{
QMediaPlaylist* playlist = m_playlistModel->playlist();
playlist->clear();
playlist->addMedia(urlList);
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
m_lrcbar->loadLyrics(m_playlistManager->localFileByIndex(index));
}
connect(playlist, &QMediaPlaylist::playbackModeChanged, this, [=](QMediaPlaylist::PlaybackMode mode) {
switch (mode) {
case QMediaPlaylist::CurrentItemInLoop:
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png"));
break;
case QMediaPlaylist::Loop:
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png"));
break;
case QMediaPlaylist::Sequential:
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-normal.png"));
break;
// case QMediaPlaylist::Random:
// ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png"));
// break;
default:
break;
void MainWindow::play()
{
m_mediaPlayer->play();
}
void MainWindow::setSkin(QString imagePath, bool save)
{
m_skin = QPixmap(imagePath);
if (save) {
saveSkinData();
}
});
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
playlist->setCurrentIndex(index < 0 ? 0 : index);
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
update();
}
void MainWindow::centerWindow()
@ -300,7 +296,7 @@ void MainWindow::on_playBtn_clicked()
{
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
loadFile();
m_mediaPlayer->play();
play();
} else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) {
ui->propLabel->setText("Error: InvalidMedia" + m_mediaPlayer->errorString());
} else {
@ -359,27 +355,18 @@ void MainWindow::on_playbackSlider_valueChanged(int value)
void MainWindow::on_prevBtn_clicked()
{
// QMediaPlaylist::previous() won't work when in CurrentItemInLoop playmode,
// and also works not as intended when in other playmode, so do it manually...
QMediaPlaylist * playlist = m_playlistModel->playlist();
if (playlist) {
int index = playlist->currentIndex();
int count = playlist->mediaCount();
playlist->setCurrentIndex(index == 0 ? count - 1 : index - 1);
}
QModelIndex index(m_playlistManager->previousIndex());
m_playlistManager->setCurrentIndex(index);
loadByModelIndex(index);
play();
}
void MainWindow::on_nextBtn_clicked()
{
// see also: MainWindow::on_prevBtn_clicked()
QMediaPlaylist * playlist = m_playlistModel->playlist();
if (playlist) {
int index = playlist->currentIndex();
int count = playlist->mediaCount();
playlist->setCurrentIndex(index == (count - 1) ? 0 : index + 1);
}
QModelIndex index(m_playlistManager->nextIndex());
m_playlistManager->setCurrentIndex(index);
loadByModelIndex(index);
play();
}
void MainWindow::on_volumeBtn_clicked()
@ -394,8 +381,8 @@ void MainWindow::on_minimumWindowBtn_clicked()
void MainWindow::initUiAndAnimation()
{
m_bgLinearGradient.setColorAt(0, QColor(255, 255, 255, 25)); // a:0
m_bgLinearGradient.setColorAt(1, QColor(255, 255, 255, 75)); // a:200
m_bgLinearGradient.setColorAt(0, QColor(0, 0, 0, 25));
m_bgLinearGradient.setColorAt(1, QColor(0, 0, 0, 80));
m_bgLinearGradient.setStart(0, 0);
m_bgLinearGradient.setFinalStop(0, height());
@ -404,22 +391,18 @@ void MainWindow::initUiAndAnimation()
m_fadeOutAnimation->setStartValue(1);
m_fadeOutAnimation->setEndValue(0);
connect(m_fadeOutAnimation, &QPropertyAnimation::finished, this, &QMainWindow::close);
// temp: a playlist for debug...
QListView * tmp_listview = new QListView(ui->pluginWidget);
tmp_listview->setModel(m_playlistModel);
tmp_listview->setGeometry({0,0,490,250});
this->setGeometry({0,0,490,160}); // temp size, hide the playlist thing.
setFixedSize(miniSize);
}
void MainWindow::initConnections()
{
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentIndexChanged, this, [=](int currentItem) {
bool isPlaying = m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState;
m_mediaPlayer->setSource(m_playlistModel->playlist()->currentMedia());
if (isPlaying) m_mediaPlayer->play();
connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, [=]{
m_audioOutput->setDevice(m_mediaDevices->defaultAudioOutput());
});
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentMediaChanged, this, [=](const QUrl &fileUrl) {
connect(m_mediaPlayer, &QMediaPlayer::sourceChanged, this, [=](){
QUrl fileUrl(m_mediaPlayer->source());
ui->titleLabel->setText(fileUrl.fileName());
ui->titleLabel->setToolTip(fileUrl.fileName());
@ -443,32 +426,29 @@ void MainWindow::initConnections()
QString::fromStdString(tag->album().to8Bit(true)));
}
#endif // NO_TAGLIB
using namespace spID3;
using namespace spFLAC;
bool coverLoaded = false;
if (suffix == "MP3") {
if (spID3::loadPictureData(filePath.toLocal8Bit().data())) {
coverLoaded = true;
QByteArray picData((const char*)spID3::getPictureDataPtr(), spID3::getPictureLength());
ui->coverLabel->setPixmap(QPixmap::fromImage(QImage::fromData(picData)));
spID3::freePictureData();
}
} else if (suffix == "FLAC") {
if (spFLAC::loadPictureData(filePath.toLocal8Bit().data())) {
coverLoaded = true;
QByteArray picData((const char*)spFLAC::getPictureDataPtr(), spFLAC::getPictureLength());
ui->coverLabel->setPixmap(QPixmap::fromImage(QImage::fromData(picData)));
spFLAC::freePictureData();
}
}
});
if (!coverLoaded) {
connect(m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, [=](){
QMediaMetaData metadata(m_mediaPlayer->metaData());
// it's known in some cases QMediaMetaData using the incorrect text codec for metadata
// see `02 Yoiyami Hanabi.mp3`'s Title. So we don't use Qt's one if tablib is available.
qDebug() << metadata.stringValue(QMediaMetaData::Title) << metadata.stringValue(QMediaMetaData::Author);
#ifdef NO_TAGLIB
setAudioMetadataForDisplay(metadata.stringValue(QMediaMetaData::Title),
metadata.stringValue(QMediaMetaData::Author),
metadata.stringValue(QMediaMetaData::AlbumTitle));
#endif // NO_TAGLIB
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
if (!coverArt.isNull()) {
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
} else {
qDebug() << "No ThumbnailImage!" << metadata.keys();
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
}
}
});
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
ui->playlistView->setCurrentIndex(m_playlistManager->model()->index(index));
});
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
@ -476,6 +456,7 @@ void MainWindow::initConnections()
if (m_mediaPlayer->duration() != 0) {
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
}
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
});
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
@ -506,35 +487,148 @@ void MainWindow::initConnections()
ui->volumeSlider->setValue(vol * 100);
});
// connect(m_mediaPlayer, static_cast<void(QMediaPlayer::*)(QMediaPlayer::Error)>(&QMediaPlayer::error),
// this, [=](QMediaPlayer::Error error) {
// switch (error) {
// default:
// break;
// }
// qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data());
// });
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, [=](QMediaPlayer::MediaStatus status){
if (status == QMediaPlayer::EndOfMedia) {
switch (m_playbackMode) {
case MainWindow::CurrentItemOnce:
// do nothing
break;
case MainWindow::CurrentItemInLoop:
// also do nothing
// as long as we did `setLoops(Infinite)`, we won't even get there
break;
case MainWindow::Sequential:
on_nextBtn_clicked();
break;
}
}
});
connect(this, &MainWindow::playbackModeChanged, this, [=](){
switch (m_playbackMode) {
case MainWindow::CurrentItemOnce:
m_mediaPlayer->setLoops(QMediaPlayer::Once);
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-repeat-single.png"));
break;
case MainWindow::CurrentItemInLoop:
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png"));
break;
case MainWindow::Sequential:
m_mediaPlayer->setLoops(QMediaPlayer::Once);
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png"));
break;
}
});
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
qDebug() << error << errorString;
});
}
void MainWindow::loadSkinData()
{
QFile file(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/skin.dat");
bool canOpen = file.open(QIODevice::ReadOnly);
if (!canOpen) return;
QDataStream stream(&file);
quint32 magic;
stream >> magic;
if (magic == 0x78297000) {
stream >> m_skin;
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
}
file.close();
}
void MainWindow::saveSkinData()
{
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if (!configDir.exists()) {
configDir.mkpath(".");
}
QFile file(configDir.absoluteFilePath("skin.dat"));
file.open(QIODevice::WriteOnly);
QDataStream stream(&file);
stream << (quint32)0x78297000 << m_skin;
file.close();
}
void MainWindow::on_playbackModeBtn_clicked()
{
QMediaPlaylist * playlist = m_playlistModel->playlist();
if (!playlist) return;
switch (playlist->playbackMode()) {
case QMediaPlaylist::CurrentItemInLoop:
playlist->setPlaybackMode(QMediaPlaylist::Loop);
switch (m_playbackMode) {
case MainWindow::CurrentItemOnce:
setProperty("playbackMode", MainWindow::CurrentItemInLoop);
break;
case QMediaPlaylist::Loop:
playlist->setPlaybackMode(QMediaPlaylist::Sequential);
case MainWindow::CurrentItemInLoop:
setProperty("playbackMode", MainWindow::Sequential);
break;
case QMediaPlaylist::Sequential:
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
case MainWindow::Sequential:
setProperty("playbackMode", MainWindow::CurrentItemOnce);
break;
// case QMediaPlaylist::Random:
// playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
// break;
default:
break;
}
}
void MainWindow::on_setSkinBtn_clicked()
{
QStringList imageFolders(QStandardPaths::standardLocations(QStandardPaths::PicturesLocation));
imageFolders.append(QDir::homePath());
QString image = QFileDialog::getOpenFileName(this, tr("Select image as background skin"),
imageFolders.first(),
tr("Image files (*.jpg *.jpeg *.png *.gif)"));
if(!image.isEmpty()) {
setSkin(image, true);
}
}
void MainWindow::on_playListBtn_clicked()
{
setFixedSize(size().height() < 200 ? fullSize : miniSize);
}
void MainWindow::on_playlistView_activated(const QModelIndex &index)
{
m_playlistManager->setCurrentIndex(index);
loadByModelIndex(index);
play();
}
void MainWindow::on_lrcBtn_clicked()
{
if (m_lrcbar->isVisible()) {
m_lrcbar->hide();
} else {
m_lrcbar->show();
}
}
void MainWindow::on_actionHelp_triggered()
{
QMessageBox infoBox(this);
infoBox.setIcon(QMessageBox::Information);
infoBox.setWindowTitle(tr("About"));
infoBox.setStandardButtons(QMessageBox::Ok);
infoBox.setText(
tr("Pineapple Music") %
"\n\n" %
tr("Based on the following free software libraries:") %
"\n\n" %
QStringLiteral("- [Qt](https://www.qt.io/) %1\n").arg(QT_VERSION_STR) %
#ifndef NO_TAGLIB
QStringLiteral("- [TagLib](https://github.com/taglib/taglib)\n") %
#endif // NO_TAGLIB
#ifndef NO_UCHARDET
QStringLiteral("- [uchardet](https://www.freedesktop.org/wiki/Software/uchardet/)\n") %
#endif // NO_TAGLIB
"\n"
"[Source Code](https://github.com/BLumia/pineapple-music)\n"
"\n"
"Copyright &copy; 2024 [BLumia](https://github.com/BLumia/)"
);
infoBox.setTextFormat(Qt::MarkdownText);
infoBox.exec();
}

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
@ -7,22 +11,32 @@
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
class QMediaDevices;
class QMediaPlayer;
class QAudioOutput;
class QPropertyAnimation;
QT_END_NAMESPACE
class PlaylistModel;
class LrcBar;
class PlaylistManager;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
enum PlaybackMode {
CurrentItemOnce,
CurrentItemInLoop,
Sequential,
};
Q_ENUM(PlaybackMode)
Q_PROPERTY(PlaybackMode playbackMode MEMBER m_playbackMode NOTIFY playbackModeChanged)
MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
void commandlinePlayAudioFiles(QStringList audioFiles);
void loadPlaylistBySingleLocalFile(const QString &path);
void setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt);
void setAudioMetadataForDisplay(QString title, QString artist, QString album);
@ -39,8 +53,12 @@ protected:
void dropEvent(QDropEvent *e) override;
void loadFile();
void loadByModelIndex(const QModelIndex &index);
void play();
void setSkin(QString imagePath, bool save);
void centerWindow();
void createPlaylist(QList<QUrl> urlList, int index = -1);
private slots:
void on_playbackModeBtn_clicked();
@ -53,22 +71,37 @@ private slots:
void on_nextBtn_clicked();
void on_volumeBtn_clicked();
void on_minimumWindowBtn_clicked();
void on_setSkinBtn_clicked();
void on_playListBtn_clicked();
void on_playlistView_activated(const QModelIndex &index);
void on_lrcBtn_clicked();
void on_actionHelp_triggered();
signals:
void playbackModeChanged(enum PlaybackMode mode);
private:
bool m_clickedOnWindow = false;
bool m_playbackSliderPressed = false;
QLinearGradient m_bgLinearGradient;
QPixmap m_skin;
enum PlaybackMode m_playbackMode = CurrentItemInLoop;
Ui::MainWindow *ui;
QMediaDevices *m_mediaDevices;
QMediaPlayer *m_mediaPlayer;
QAudioOutput *m_audioOutput;
LrcBar *m_lrcbar;
QPropertyAnimation *m_fadeOutAnimation;
PlaylistModel *m_playlistModel = nullptr; // TODO: move playback logic to player.cpp
PlaylistManager *m_playlistManager;
void initUiAndAnimation();
void initConnections();
void loadSkinData();
void saveSkinData();
static QString ms2str(qint64 ms);
static QList<QUrl> strlst2urllst(QStringList strlst);
};

View File

@ -20,7 +20,7 @@
<bool>true</bool>
</property>
<property name="windowTitle">
<string>Pineapple Player</string>
<string>Pineapple Music</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
@ -77,11 +77,11 @@ QPushButton {
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 220);
background-color: rgba(0, 85, 255, 120);
}
QPushButton:pressed {
background-color: rgba(255, 255, 255, 250);
background-color: rgba(0, 85, 255, 250);
}
QPushButton#closeWindowBtn {
@ -101,6 +101,12 @@ QLabel {
QLabel#coverLabel {
border: 1px solid grey;
}
/****** ListView ******/
QListView {
background: rgba(0, 0, 0, 50);
}</string>
</property>
<property name="locale">
@ -115,7 +121,7 @@ QLabel#coverLabel {
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<property name="spacing">
<number>7</number>
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
@ -209,7 +215,7 @@ QLabel#coverLabel {
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -220,7 +226,7 @@ QLabel#coverLabel {
</spacer>
</item>
<item>
<widget class="QPushButton" name="miniModeBtn">
<widget class="QPushButton" name="setSkinBtn">
<property name="maximumSize">
<size>
<width>25</width>
@ -228,7 +234,17 @@ QLabel#coverLabel {
</size>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/icons/icons/skin.png</normaloff>:/icons/icons/skin.png</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
@ -277,7 +293,7 @@ QLabel#coverLabel {
<item>
<layout class="QVBoxLayout" name="playerPanelLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property>
<property name="rightMargin">
<number>10</number>
@ -298,6 +314,11 @@ QLabel#coverLabel {
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="propLabel">
<property name="text">
@ -305,22 +326,43 @@ QLabel#coverLabel {
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="lrcBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string comment="Lyrics">Lrc</string>
</property>
<property name="iconSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="playbackTimeLayout">
<item>
<widget class="QLabel" name="nowTimeLabel">
<property name="text">
<string>0:00</string>
<string notr="true">0:00</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="totalTimeLabel">
<property name="text">
<string>0:00</string>
<string notr="true">0:00</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
@ -332,7 +374,7 @@ QLabel#coverLabel {
<number>1000</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -562,7 +604,7 @@ QLabel#coverLabel {
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@ -576,9 +618,9 @@ QLabel#coverLabel {
</widget>
</item>
<item>
<widget class="QWidget" name="pluginWidget" native="true">
<widget class="QStackedWidget" name="pluginStackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@ -589,10 +631,43 @@ QLabel#coverLabel {
<height>0</height>
</size>
</property>
<widget class="QWidget" name="pluginStackedWidgetPage1">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QListView" name="playlistView"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<action name="actionHelp">
<property name="text">
<string>help</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

255
playlistmanager.cpp Normal file
View File

@ -0,0 +1,255 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "playlistmanager.h"
#include <QCollator>
#include <QDir>
#include <QFileInfo>
#include <QUrl>
PlaylistModel::PlaylistModel(QObject *parent)
: QAbstractListModel(parent)
{
}
PlaylistModel::~PlaylistModel()
{
}
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
{
beginResetModel();
m_playlist = urls;
endResetModel();
}
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
{
if (urls.isEmpty()) return QModelIndex();
if (urls.count() == 1) {
return loadPlaylist(urls.constFirst());
} else {
setPlaylist(urls);
return index(0);
}
}
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
{
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString && currentFileName = info.fileName();
if (dir.path() == m_currentDir) {
int idx = indexOf(url);
return idx == -1 ? appendToPlaylist(url) : index(idx);
}
QStringList entryList = dir.entryList(
m_autoLoadSuffixes,
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> playlist;
int idx = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & fileName = entryList.at(i);
const QString & oneEntry = dir.absoluteFilePath(fileName);
const QUrl & url = QUrl::fromLocalFile(oneEntry);
playlist.append(url);
if (fileName == currentFileName) {
idx = i;
}
}
if (idx == -1) {
idx = playlist.count();
playlist.append(url);
}
m_currentDir = dir.path();
setPlaylist(playlist);
return index(idx);
}
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
{
const int lastIndex = rowCount();
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
m_playlist.append(url);
endInsertRows();
return index(lastIndex);
}
bool PlaylistModel::removeAt(int index)
{
if (index < 0 || index >= rowCount()) return false;
beginRemoveRows(QModelIndex(), index, index);
m_playlist.removeAt(index);
endRemoveRows();
return true;
}
int PlaylistModel::indexOf(const QUrl &url) const
{
return m_playlist.indexOf(url);
}
QUrl PlaylistModel::urlByIndex(int index) const
{
return m_playlist.value(index);
}
QStringList PlaylistModel::autoLoadFilterSuffixes() const
{
return m_autoLoadSuffixes;
}
QHash<int, QByteArray> PlaylistModel::roleNames() const
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result.insert(UrlRole, "url");
return result;
}
int PlaylistModel::rowCount(const QModelIndex &parent) const
{
return m_playlist.count();
}
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
switch (role) {
case Qt::DisplayRole:
return m_playlist.at(index.row()).fileName();
case UrlRole:
return m_playlist.at(index.row());
}
return QVariant();
}
PlaylistManager::PlaylistManager(QObject *parent)
: QObject(parent)
{
connect(&m_model, &PlaylistModel::rowsRemoved, this,
[this](const QModelIndex &, int, int) {
if (m_model.rowCount() <= m_currentIndex) {
setProperty("currentIndex", m_currentIndex - 1);
}
});
auto onRowCountChanged = [this](){
emit totalCountChanged(m_model.rowCount());
};
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
}
PlaylistManager::~PlaylistManager()
{
}
PlaylistModel *PlaylistManager::model()
{
return &m_model;
}
void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
{
m_model.setPlaylist(urls);
}
QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
{
QModelIndex idx = m_model.loadPlaylist(urls);
setProperty("currentIndex", idx.row());
return idx;
}
QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
{
QModelIndex idx = m_model.loadPlaylist(url);
setProperty("currentIndex", idx.row());
return idx;
}
int PlaylistManager::totalCount() const
{
return m_model.rowCount();
}
QModelIndex PlaylistManager::previousIndex() const
{
int count = totalCount();
if (count == 0) return QModelIndex();
return m_model.index(m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1);
}
QModelIndex PlaylistManager::nextIndex() const
{
int count = totalCount();
if (count == 0) return QModelIndex();
return m_model.index(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1);
}
QModelIndex PlaylistManager::curIndex() const
{
return m_model.index(m_currentIndex);
}
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
{
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
setProperty("currentIndex", index.row());
}
}
QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
{
return m_model.urlByIndex(index.row());
}
QString PlaylistManager::localFileByIndex(const QModelIndex &index)
{
return urlByIndex(index).toLocalFile();
}
bool PlaylistManager::removeAt(const QModelIndex &index)
{
return m_model.removeAt(index.row());
}
void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
{
m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
}
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)
{
QList<QUrl> urlList;
for (const QString & str : std::as_const(files)) {
QUrl url = QUrl::fromLocalFile(str);
if (url.isValid()) {
urlList.append(url);
}
}
return urlList;
}

85
playlistmanager.h Normal file
View File

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QUrl>
#include <QAbstractListModel>
class PlaylistModel : public QAbstractListModel
{
Q_OBJECT
public:
enum PlaylistRole {
UrlRole = Qt::UserRole
};
Q_ENUM(PlaylistRole)
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
explicit PlaylistModel(QObject *parent = nullptr);
~PlaylistModel();
void setPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QUrl & url);
QModelIndex appendToPlaylist(const QUrl & url);
bool removeAt(int index);
int indexOf(const QUrl & url) const;
QUrl urlByIndex(int index) const;
QStringList autoLoadFilterSuffixes() const;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void autoLoadFilterSuffixesChanged(QStringList suffixes);
private:
// model data
QList<QUrl> m_playlist;
// properties
QStringList m_autoLoadSuffixes = {};
// internal
QString m_currentDir;
};
class PlaylistManager : public QObject
{
Q_OBJECT
public:
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes)
Q_PROPERTY(PlaylistModel * model READ model CONSTANT)
explicit PlaylistManager(QObject *parent = nullptr);
~PlaylistManager();
PlaylistModel * model();
void setPlaylist(const QList<QUrl> & url);
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
int totalCount() const;
QModelIndex previousIndex() const;
QModelIndex nextIndex() const;
QModelIndex curIndex() const;
void setCurrentIndex(const QModelIndex & index);
QUrl urlByIndex(const QModelIndex & index);
QString localFileByIndex(const QModelIndex & index);
bool removeAt(const QModelIndex & index);
void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
static QList<QUrl> convertToUrlList(const QStringList & files);
signals:
void currentIndexChanged(int index);
void totalCountChanged(int count);
private:
int m_currentIndex = -1;
PlaylistModel m_model;
};

View File

@ -1,102 +0,0 @@
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "playlistmodel.h"
#include "qt/qmediaplaylist.h"
#include <QFileInfo>
#include <QUrl>
PlaylistModel::PlaylistModel(QObject *parent)
: QAbstractItemModel(parent)
{
m_playlist.reset(new QMediaPlaylist);
connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems);
connect(m_playlist.data(), &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems);
connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems);
connect(m_playlist.data(), &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems);
connect(m_playlist.data(), &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems);
}
PlaylistModel::~PlaylistModel() = default;
int PlaylistModel::rowCount(const QModelIndex &parent) const
{
return m_playlist && !parent.isValid() ? m_playlist->mediaCount() : 0;
}
int PlaylistModel::columnCount(const QModelIndex &parent) const
{
return !parent.isValid() ? ColumnCount : 0;
}
QModelIndex PlaylistModel::index(int row, int column, const QModelIndex &parent) const
{
return m_playlist && !parent.isValid()
&& row >= 0 && row < m_playlist->mediaCount()
&& column >= 0 && column < ColumnCount
? createIndex(row, column)
: QModelIndex();
}
QModelIndex PlaylistModel::parent(const QModelIndex &child) const
{
Q_UNUSED(child);
return QModelIndex();
}
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (index.isValid() && role == Qt::DisplayRole) {
QVariant value = m_data[index];
if (!value.isValid() && index.column() == Title) {
QUrl location = m_playlist->media(index.row());
return QFileInfo(location.path()).fileName();
}
return value;
}
return QVariant();
}
QMediaPlaylist *PlaylistModel::playlist() const
{
return m_playlist.data();
}
bool PlaylistModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_UNUSED(role);
m_data[index] = value;
emit dataChanged(index, index);
return true;
}
void PlaylistModel::beginInsertItems(int start, int end)
{
m_data.clear();
beginInsertRows(QModelIndex(), start, end);
}
void PlaylistModel::endInsertItems()
{
endInsertRows();
}
void PlaylistModel::beginRemoveItems(int start, int end)
{
m_data.clear();
beginRemoveRows(QModelIndex(), start, end);
}
void PlaylistModel::endRemoveItems()
{
endInsertRows();
}
void PlaylistModel::changeItems(int start, int end)
{
m_data.clear();
emit dataChanged(index(start,0), index(end,ColumnCount));
}

View File

@ -1,52 +0,0 @@
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef PLAYLISTMODEL_H
#define PLAYLISTMODEL_H
#include <QAbstractItemModel>
#include <QScopedPointer>
QT_BEGIN_NAMESPACE
class QMediaPlaylist;
QT_END_NAMESPACE
class PlaylistModel : public QAbstractItemModel
{
Q_OBJECT
public:
enum Column
{
Title = 0,
ColumnCount
};
explicit PlaylistModel(QObject *parent = nullptr);
~PlaylistModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QMediaPlaylist *playlist() const;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override;
private slots:
void beginInsertItems(int start, int end);
void endInsertItems();
void beginRemoveItems(int start, int end);
void endRemoveItems();
void changeItems(int start, int end);
private:
QScopedPointer<QMediaPlaylist> m_playlist;
QMap<QModelIndex, QVariant> m_data;
};
#endif // PLAYLISTMODEL_H

View File

@ -1,653 +0,0 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qmediaplaylist.h"
#include "qmediaplaylist_p.h"
#include "qplaylistfileparser_p.h"
#include <QtCore/qlist.h>
#include <QtCore/qfile.h>
#include <QtCore/qurl.h>
#include <QtCore/qcoreevent.h>
#include <QtCore/qcoreapplication.h>
#include <QRandomGenerator>
QT_BEGIN_NAMESPACE
class QM3uPlaylistWriter
{
public:
QM3uPlaylistWriter(QIODevice *device)
:m_device(device), m_textStream(new QTextStream(m_device))
{
}
~QM3uPlaylistWriter()
{
delete m_textStream;
}
bool writeItem(const QUrl& item)
{
*m_textStream << item.toString() << Qt::endl;
return true;
}
private:
QIODevice *m_device;
QTextStream *m_textStream;
};
int QMediaPlaylistPrivate::nextPosition(int steps) const
{
if (playlist.count() == 0)
return -1;
int next = currentPos + steps;
switch (playbackMode) {
case QMediaPlaylist::CurrentItemOnce:
return steps != 0 ? -1 : currentPos;
case QMediaPlaylist::CurrentItemInLoop:
return currentPos;
case QMediaPlaylist::Sequential:
if (next >= playlist.size())
next = -1;
break;
case QMediaPlaylist::Loop:
next %= playlist.count();
break;
}
return next;
}
int QMediaPlaylistPrivate::prevPosition(int steps) const
{
if (playlist.count() == 0)
return -1;
int next = currentPos;
if (next < 0)
next = playlist.size();
next -= steps;
switch (playbackMode) {
case QMediaPlaylist::CurrentItemOnce:
return steps != 0 ? -1 : currentPos;
case QMediaPlaylist::CurrentItemInLoop:
return currentPos;
case QMediaPlaylist::Sequential:
if (next < 0)
next = -1;
break;
case QMediaPlaylist::Loop:
next %= playlist.size();
if (next < 0)
next += playlist.size();
break;
}
return next;
}
/*!
\class QMediaPlaylist
\inmodule QtMultimedia
\ingroup multimedia
\ingroup multimedia_playback
\brief The QMediaPlaylist class provides a list of media content to play.
QMediaPlaylist is intended to be used with other media objects,
like QMediaPlayer.
QMediaPlaylist allows to access the service intrinsic playlist functionality
if available, otherwise it provides the local memory playlist implementation.
\snippet multimedia-snippets/media.cpp Movie playlist
Depending on playlist source implementation, most of the playlist mutating
operations can be asynchronous.
QMediaPlayList currently supports M3U playlists (file extension .m3u and .m3u8).
\sa QUrl
*/
/*!
\enum QMediaPlaylist::PlaybackMode
The QMediaPlaylist::PlaybackMode describes the order items in playlist are played.
\value CurrentItemOnce The current item is played only once.
\value CurrentItemInLoop The current item is played repeatedly in a loop.
\value Sequential Playback starts from the current and moves through each successive item until the last is reached and then stops.
The next item is a null item when the last one is currently playing.
\value Loop Playback restarts at the first item after the last has finished playing.
\value Random Play items in random order.
*/
/*!
Create a new playlist object with the given \a parent.
*/
QMediaPlaylist::QMediaPlaylist(QObject *parent)
: QObject(parent)
, d_ptr(new QMediaPlaylistPrivate)
{
Q_D(QMediaPlaylist);
d->q_ptr = this;
}
/*!
Destroys the playlist.
*/
QMediaPlaylist::~QMediaPlaylist()
{
delete d_ptr;
}
/*!
\property QMediaPlaylist::playbackMode
This property defines the order that items in the playlist are played.
\sa QMediaPlaylist::PlaybackMode
*/
QMediaPlaylist::PlaybackMode QMediaPlaylist::playbackMode() const
{
return d_func()->playbackMode;
}
void QMediaPlaylist::setPlaybackMode(QMediaPlaylist::PlaybackMode mode)
{
Q_D(QMediaPlaylist);
if (mode == d->playbackMode)
return;
d->playbackMode = mode;
emit playbackModeChanged(mode);
}
/*!
Returns position of the current media content in the playlist.
*/
int QMediaPlaylist::currentIndex() const
{
return d_func()->currentPos;
}
/*!
Returns the current media content.
*/
QUrl QMediaPlaylist::currentMedia() const
{
Q_D(const QMediaPlaylist);
if (d->currentPos < 0 || d->currentPos >= d->playlist.size())
return QUrl();
return d_func()->playlist.at(d_func()->currentPos);
}
/*!
Returns the index of the item, which would be current after calling next()
\a steps times.
Returned value depends on the size of playlist, current position
and playback mode.
\sa QMediaPlaylist::playbackMode(), previousIndex()
*/
int QMediaPlaylist::nextIndex(int steps) const
{
return d_func()->nextPosition(steps);
}
/*!
Returns the index of the item, which would be current after calling previous()
\a steps times.
\sa QMediaPlaylist::playbackMode(), nextIndex()
*/
int QMediaPlaylist::previousIndex(int steps) const
{
return d_func()->prevPosition(steps);
}
/*!
Returns the number of items in the playlist.
\sa isEmpty()
*/
int QMediaPlaylist::mediaCount() const
{
return d_func()->playlist.count();
}
/*!
Returns true if the playlist contains no items, otherwise returns false.
\sa mediaCount()
*/
bool QMediaPlaylist::isEmpty() const
{
return mediaCount() == 0;
}
/*!
Returns the media content at \a index in the playlist.
*/
QUrl QMediaPlaylist::media(int index) const
{
Q_D(const QMediaPlaylist);
if (index < 0 || index >= d->playlist.size())
return QUrl();
return d->playlist.at(index);
}
/*!
Append the media \a content to the playlist.
Returns true if the operation is successful, otherwise returns false.
*/
void QMediaPlaylist::addMedia(const QUrl &content)
{
Q_D(QMediaPlaylist);
int pos = d->playlist.size();
emit mediaAboutToBeInserted(pos, pos);
d->playlist.append(content);
emit mediaInserted(pos, pos);
}
/*!
Append multiple media content \a items to the playlist.
Returns true if the operation is successful, otherwise returns false.
*/
void QMediaPlaylist::addMedia(const QList<QUrl> &items)
{
if (!items.size())
return;
Q_D(QMediaPlaylist);
int first = d->playlist.size();
int last = first + items.size() - 1;
emit mediaAboutToBeInserted(first, last);
d_func()->playlist.append(items);
emit mediaInserted(first, last);
}
/*!
Insert the media \a content to the playlist at position \a pos.
Returns true if the operation is successful, otherwise returns false.
*/
bool QMediaPlaylist::insertMedia(int pos, const QUrl &content)
{
Q_D(QMediaPlaylist);
pos = qBound(0, pos, d->playlist.size());
emit mediaAboutToBeInserted(pos, pos);
d->playlist.insert(pos, content);
emit mediaInserted(pos, pos);
return true;
}
/*!
Insert multiple media content \a items to the playlist at position \a pos.
Returns true if the operation is successful, otherwise returns false.
*/
bool QMediaPlaylist::insertMedia(int pos, const QList<QUrl> &items)
{
if (!items.size())
return true;
Q_D(QMediaPlaylist);
pos = qBound(0, pos, d->playlist.size());
int last = pos + items.size() - 1;
emit mediaAboutToBeInserted(pos, last);
auto newList = d->playlist.mid(0, pos);
newList += items;
newList += d->playlist.mid(pos);
d->playlist = newList;
emit mediaInserted(pos, last);
return true;
}
/*!
Move the item from position \a from to position \a to.
Returns true if the operation is successful, otherwise false.
\since 5.7
*/
bool QMediaPlaylist::moveMedia(int from, int to)
{
Q_D(QMediaPlaylist);
if (from < 0 || from > d->playlist.count() ||
to < 0 || to > d->playlist.count())
return false;
d->playlist.move(from, to);
emit mediaChanged(from, to);
return true;
}
/*!
Remove the item from the playlist at position \a pos.
Returns true if the operation is successful, otherwise return false.
*/
bool QMediaPlaylist::removeMedia(int pos)
{
return removeMedia(pos, pos);
}
/*!
Remove items in the playlist from \a start to \a end inclusive.
Returns true if the operation is successful, otherwise return false.
*/
bool QMediaPlaylist::removeMedia(int start, int end)
{
Q_D(QMediaPlaylist);
if (end < start || end < 0 || start >= d->playlist.count())
return false;
start = qBound(0, start, d->playlist.size() - 1);
end = qBound(0, end, d->playlist.size() - 1);
emit mediaAboutToBeRemoved(start, end);
d->playlist.remove(start, end - start + 1);
emit mediaRemoved(start, end);
return true;
}
/*!
Remove all the items from the playlist.
Returns true if the operation is successful, otherwise return false.
*/
void QMediaPlaylist::clear()
{
Q_D(QMediaPlaylist);
int size = d->playlist.size();
emit mediaAboutToBeRemoved(0, size - 1);
d->playlist.clear();
emit mediaRemoved(0, size - 1);
}
/*!
Load playlist from \a location. If \a format is specified, it is used,
otherwise format is guessed from location name and data.
New items are appended to playlist.
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
otherwise the playlist emits loadFailed().
*/
void QMediaPlaylist::load(const QUrl &location, const char *format)
{
Q_D(QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
d->ensureParser();
d->parser->start(location, QString::fromUtf8(format));
}
/*!
Load playlist from QIODevice \a device. If \a format is specified, it is used,
otherwise format is guessed from device data.
New items are appended to playlist.
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
otherwise the playlist emits loadFailed().
*/
void QMediaPlaylist::load(QIODevice *device, const char *format)
{
Q_D(QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
d->ensureParser();
d->parser->start(device, QString::fromUtf8(format));
}
/*!
Save playlist to \a location. If \a format is specified, it is used,
otherwise format is guessed from location name.
Returns true if playlist was saved successfully, otherwise returns false.
*/
bool QMediaPlaylist::save(const QUrl &location, const char *format) const
{
Q_D(const QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
if (!d->checkFormat(format))
return false;
QFile file(location.toLocalFile());
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
d->error = AccessDeniedError;
d->errorString = tr("The file could not be accessed.");
return false;
}
return save(&file, format);
}
/*!
Save playlist to QIODevice \a device using format \a format.
Returns true if playlist was saved successfully, otherwise returns false.
*/
bool QMediaPlaylist::save(QIODevice *device, const char *format) const
{
Q_D(const QMediaPlaylist);
d->error = NoError;
d->errorString.clear();
if (!d->checkFormat(format))
return false;
QM3uPlaylistWriter writer(device);
for (const auto &entry : d->playlist)
writer.writeItem(entry);
return true;
}
/*!
Returns the last error condition.
*/
QMediaPlaylist::Error QMediaPlaylist::error() const
{
return d_func()->error;
}
/*!
Returns the string describing the last error condition.
*/
QString QMediaPlaylist::errorString() const
{
return d_func()->errorString;
}
/*!
Shuffle items in the playlist.
*/
void QMediaPlaylist::shuffle()
{
Q_D(QMediaPlaylist);
QList<QUrl> playlist;
// keep the current item when shuffling
QUrl current;
if (d->currentPos != -1)
current = d->playlist.takeAt(d->currentPos);
while (!d->playlist.isEmpty())
playlist.append(d->playlist.takeAt(QRandomGenerator::global()->bounded(int(d->playlist.size()))));
if (d->currentPos != -1)
playlist.insert(d->currentPos, current);
d->playlist = playlist;
emit mediaChanged(0, d->playlist.count());
}
/*!
Advance to the next media content in playlist.
*/
void QMediaPlaylist::next()
{
Q_D(QMediaPlaylist);
d->currentPos = d->nextPosition(1);
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
Return to the previous media content in playlist.
*/
void QMediaPlaylist::previous()
{
Q_D(QMediaPlaylist);
d->currentPos = d->prevPosition(1);
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
Activate media content from playlist at position \a playlistPosition.
*/
void QMediaPlaylist::setCurrentIndex(int playlistPosition)
{
Q_D(QMediaPlaylist);
if (playlistPosition < 0 || playlistPosition >= d->playlist.size())
playlistPosition = -1;
d->currentPos = playlistPosition;
emit currentIndexChanged(d->currentPos);
emit currentMediaChanged(currentMedia());
}
/*!
\fn void QMediaPlaylist::mediaInserted(int start, int end)
This signal is emitted after media has been inserted into the playlist.
The new items are those between \a start and \a end inclusive.
*/
/*!
\fn void QMediaPlaylist::mediaRemoved(int start, int end)
This signal is emitted after media has been removed from the playlist.
The removed items are those between \a start and \a end inclusive.
*/
/*!
\fn void QMediaPlaylist::mediaChanged(int start, int end)
This signal is emitted after media has been changed in the playlist
between \a start and \a end positions inclusive.
*/
/*!
\fn void QMediaPlaylist::currentIndexChanged(int position)
Signal emitted when playlist position changed to \a position.
*/
/*!
\fn void QMediaPlaylist::playbackModeChanged(QMediaPlaylist::PlaybackMode mode)
Signal emitted when playback mode changed to \a mode.
*/
/*!
\fn void QMediaPlaylist::mediaAboutToBeInserted(int start, int end)
Signal emitted when items are to be inserted at \a start and ending at \a end.
*/
/*!
\fn void QMediaPlaylist::mediaAboutToBeRemoved(int start, int end)
Signal emitted when item are to be deleted at \a start and ending at \a end.
*/
/*!
\fn void QMediaPlaylist::currentMediaChanged(const QUrl &content)
Signal emitted when current media changes to \a content.
*/
/*!
\property QMediaPlaylist::currentIndex
\brief Current position.
*/
/*!
\property QMediaPlaylist::currentMedia
\brief Current media content.
*/
/*!
\fn QMediaPlaylist::loaded()
Signal emitted when playlist finished loading.
*/
/*!
\fn QMediaPlaylist::loadFailed()
Signal emitted if failed to load playlist.
*/
/*!
\enum QMediaPlaylist::Error
This enum describes the QMediaPlaylist error codes.
\value NoError No errors.
\value FormatError Format error.
\value FormatNotSupportedError Format not supported.
\value NetworkError Network error.
\value AccessDeniedError Access denied error.
*/
QT_END_NAMESPACE
#include "moc_qmediaplaylist.cpp"

View File

@ -1,96 +0,0 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QMEDIAPLAYLIST_H
#define QMEDIAPLAYLIST_H
#include <QtCore/qobject.h>
#include <QtMultimedia/qtmultimediaglobal.h>
#include <QtMultimedia/qmediaenumdebug.h>
QT_BEGIN_NAMESPACE
class QMediaPlaylistPrivate;
class QMediaPlaylist : public QObject
{
Q_OBJECT
Q_PROPERTY(QMediaPlaylist::PlaybackMode playbackMode READ playbackMode WRITE setPlaybackMode NOTIFY playbackModeChanged)
Q_PROPERTY(QUrl currentMedia READ currentMedia NOTIFY currentMediaChanged)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
public:
enum PlaybackMode { CurrentItemOnce, CurrentItemInLoop, Sequential, Loop };
Q_ENUM(PlaybackMode)
enum Error { NoError, FormatError, FormatNotSupportedError, NetworkError, AccessDeniedError };
Q_ENUM(Error)
explicit QMediaPlaylist(QObject *parent = nullptr);
virtual ~QMediaPlaylist();
PlaybackMode playbackMode() const;
void setPlaybackMode(PlaybackMode mode);
int currentIndex() const;
QUrl currentMedia() const;
int nextIndex(int steps = 1) const;
int previousIndex(int steps = 1) const;
QUrl media(int index) const;
int mediaCount() const;
bool isEmpty() const;
void addMedia(const QUrl &content);
void addMedia(const QList<QUrl> &items);
bool insertMedia(int index, const QUrl &content);
bool insertMedia(int index, const QList<QUrl> &items);
bool moveMedia(int from, int to);
bool removeMedia(int pos);
bool removeMedia(int start, int end);
void clear();
void load(const QUrl &location, const char *format = nullptr);
void load(QIODevice *device, const char *format = nullptr);
bool save(const QUrl &location, const char *format = nullptr) const;
bool save(QIODevice *device, const char *format) const;
Error error() const;
QString errorString() const;
public Q_SLOTS:
void shuffle();
void next();
void previous();
void setCurrentIndex(int index);
Q_SIGNALS:
void currentIndexChanged(int index);
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
void currentMediaChanged(const QUrl&);
void mediaAboutToBeInserted(int start, int end);
void mediaInserted(int start, int end);
void mediaAboutToBeRemoved(int start, int end);
void mediaRemoved(int start, int end);
void mediaChanged(int start, int end);
void loaded();
void loadFailed();
private:
QMediaPlaylistPrivate *d_ptr;
Q_DECLARE_PRIVATE(QMediaPlaylist)
};
QT_END_NAMESPACE
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode)
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error)
#endif // QMEDIAPLAYLIST_H

View File

@ -1,112 +0,0 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QMEDIAPLAYLIST_P_H
#define QMEDIAPLAYLIST_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qmediaplaylist.h"
#include "qplaylistfileparser_p.h"
#include <QtCore/qdebug.h>
#ifdef Q_MOC_RUN
# pragma Q_MOC_EXPAND_MACROS
#endif
QT_BEGIN_NAMESPACE
class QMediaPlaylistControl;
class QMediaPlaylistPrivate
{
Q_DECLARE_PUBLIC(QMediaPlaylist)
public:
QMediaPlaylistPrivate()
: error(QMediaPlaylist::NoError)
{
}
virtual ~QMediaPlaylistPrivate()
{
if (parser)
delete parser;
}
void loadFailed(QMediaPlaylist::Error error, const QString &errorString)
{
this->error = error;
this->errorString = errorString;
emit q_ptr->loadFailed();
}
void loadFinished()
{
q_ptr->addMedia(parser->playlist);
emit q_ptr->loaded();
}
bool checkFormat(const char *format) const
{
QLatin1String f(format);
QPlaylistFileParser::FileType type = format ? QPlaylistFileParser::UNKNOWN : QPlaylistFileParser::M3U8;
if (format) {
if (f == QLatin1String("m3u") || f == QLatin1String("text/uri-list") ||
f == QLatin1String("audio/x-mpegurl") || f == QLatin1String("audio/mpegurl"))
type = QPlaylistFileParser::M3U;
else if (f == QLatin1String("m3u8") || f == QLatin1String("application/x-mpegURL") ||
f == QLatin1String("application/vnd.apple.mpegurl"))
type = QPlaylistFileParser::M3U8;
}
if (type == QPlaylistFileParser::UNKNOWN || type == QPlaylistFileParser::PLS) {
error = QMediaPlaylist::FormatNotSupportedError;
errorString = QMediaPlaylist::tr("This file format is not supported.");
return false;
}
return true;
}
void ensureParser()
{
if (parser)
return;
parser = new QPlaylistFileParser(q_ptr);
QObject::connect(parser, &QPlaylistFileParser::finished, [this]() { loadFinished(); });
QObject::connect(parser, &QPlaylistFileParser::error,
[this](QMediaPlaylist::Error err, const QString& errorMsg) { loadFailed(err, errorMsg); });
}
int nextPosition(int steps) const;
int prevPosition(int steps) const;
QList<QUrl> playlist;
int currentPos = -1;
QMediaPlaylist::PlaybackMode playbackMode = QMediaPlaylist::Sequential;
QPlaylistFileParser *parser = nullptr;
mutable QMediaPlaylist::Error error;
mutable QString errorString;
QMediaPlaylist *q_ptr;
};
QT_END_NAMESPACE
#endif // QMEDIAPLAYLIST_P_H

View File

@ -1,605 +0,0 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qplaylistfileparser_p.h"
#include <qfileinfo.h>
#include <QtCore/QDebug>
#include <QtCore/qiodevice.h>
#include <QtCore/qpointer.h>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include "qmediaplayer.h"
#include "qmediametadata.h"
QT_BEGIN_NAMESPACE
namespace {
class ParserBase
{
public:
explicit ParserBase(QPlaylistFileParser *parent)
: m_parent(parent)
, m_aborted(false)
{
Q_ASSERT(m_parent);
}
bool parseLine(int lineIndex, const QString& line, const QUrl& root)
{
if (m_aborted)
return false;
const bool ok = parseLineImpl(lineIndex, line, root);
return ok && !m_aborted;
}
virtual void abort() { m_aborted = true; }
virtual ~ParserBase() = default;
protected:
virtual bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) = 0;
static QUrl expandToFullPath(const QUrl &root, const QString &line)
{
// On Linux, backslashes are not converted to forward slashes :/
if (line.startsWith(QLatin1String("//")) || line.startsWith(QLatin1String("\\\\"))) {
// Network share paths are not resolved
return QUrl::fromLocalFile(line);
}
QUrl url(line);
if (url.scheme().isEmpty()) {
// Resolve it relative to root
if (root.isLocalFile())
return QUrl::fromUserInput(line, root.adjusted(QUrl::RemoveFilename).toLocalFile(), QUrl::AssumeLocalFile);
return root.resolved(url);
}
if (url.scheme().length() == 1)
// Assume it's a drive letter for a Windows path
url = QUrl::fromLocalFile(line);
return url;
}
void newItemFound(const QVariant& content) { Q_EMIT m_parent->newItem(content); }
QPlaylistFileParser *m_parent;
bool m_aborted;
};
class M3UParser : public ParserBase
{
public:
explicit M3UParser(QPlaylistFileParser *q)
: ParserBase(q)
, m_extendedFormat(false)
{
}
/*
*
Extended M3U directives
#EXTM3U - header - must be first line of file
#EXTINF - extra info - length (seconds), title
#EXTINF - extra info - length (seconds), artist '-' title
Example
#EXTM3U
#EXTINF:123, Sample artist - Sample title
C:\Documents and Settings\I\My Music\Sample.mp3
#EXTINF:321,Example Artist - Example title
C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg
*/
bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) override
{
if (line[0] == u'#' ) {
if (m_extendedFormat) {
if (line.startsWith(QLatin1String("#EXTINF:"))) {
m_extraInfo.clear();
int artistStart = line.indexOf(QLatin1String(","), 8);
bool ok = false;
QStringView lineView { line };
int length = lineView.mid(8, artistStart < 8 ? -1 : artistStart - 8).trimmed().toInt(&ok);
if (ok && length > 0) {
//convert from second to milisecond
m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000);
}
if (artistStart > 0) {
int titleStart = getSplitIndex(line, artistStart);
if (titleStart > artistStart) {
m_extraInfo[QMediaMetaData::Author] = lineView.mid(artistStart + 1,
titleStart - artistStart - 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
m_extraInfo[QMediaMetaData::Title] = lineView.mid(titleStart + 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
} else {
m_extraInfo[QMediaMetaData::Title] = lineView.mid(artistStart + 1).trimmed().toString().
replace(QLatin1String("--"), QLatin1String("-"));
}
}
}
} else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) {
m_extendedFormat = true;
}
} else {
QUrl url = expandToFullPath(root, line);
m_extraInfo[QMediaMetaData::Url] = url;
m_parent->playlist.append(url);
newItemFound(QVariant::fromValue(m_extraInfo));
m_extraInfo.clear();
}
return true;
}
int getSplitIndex(const QString& line, int startPos)
{
if (startPos < 0)
startPos = 0;
const QChar* buf = line.data();
for (int i = startPos; i < line.length(); ++i) {
if (buf[i] == u'-') {
if (i == line.length() - 1)
return i;
++i;
if (buf[i] != u'-')
return i - 1;
}
}
return -1;
}
private:
QMediaMetaData m_extraInfo;
bool m_extendedFormat;
};
class PLSParser : public ParserBase
{
public:
explicit PLSParser(QPlaylistFileParser *q)
: ParserBase(q)
{
}
/*
*
The format is essentially that of an INI file structured as follows:
Header
* [playlist] : This tag indicates that it is a Playlist File
Track Entry
Assuming track entry #X
* FileX : Variable defining location of stream.
* TitleX : Defines track title.
* LengthX : Length in seconds of track. Value of -1 indicates indefinite.
Footer
* NumberOfEntries : This variable indicates the number of tracks.
* Version : Playlist version. Currently only a value of 2 is valid.
[playlist]
File1=Alternative\everclear - SMFTA.mp3
Title1=Everclear - So Much For The Afterglow
Length1=233
File2=http://www.site.com:8000/listen.pls
Title2=My Cool Stream
Length5=-1
NumberOfEntries=2
Version=2
*/
bool parseLineImpl(int, const QString &line, const QUrl &root) override
{
// We ignore everything but 'File' entries, since that's the only thing we care about.
if (!line.startsWith(QLatin1String("File")))
return true;
QString value = getValue(line);
if (value.isEmpty())
return true;
QUrl path = expandToFullPath(root, value);
m_parent->playlist.append(path);
newItemFound(path);
return true;
}
QString getValue(QStringView line) {
int start = line.indexOf(u'=');
if (start < 0)
return QString();
return line.mid(start + 1).trimmed().toString();
}
};
}
/////////////////////////////////////////////////////////////////////////////////////////////////
class QPlaylistFileParserPrivate
{
Q_DECLARE_PUBLIC(QPlaylistFileParser)
public:
QPlaylistFileParserPrivate(QPlaylistFileParser *q)
: q_ptr(q)
, m_stream(nullptr)
, m_type(QPlaylistFileParser::UNKNOWN)
, m_scanIndex(0)
, m_lineIndex(-1)
, m_utf8(false)
, m_aborted(false)
{
}
void handleData();
void handleParserFinished();
void abort();
void reset();
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source;
QScopedPointer<ParserBase> m_currentParser;
QByteArray m_buffer;
QUrl m_root;
QNetworkAccessManager m_mgr;
QString m_mimeType;
QPlaylistFileParser *q_ptr;
QPointer<QIODevice> m_stream;
QPlaylistFileParser::FileType m_type;
struct ParserJob
{
QIODevice *m_stream;
QUrl m_media;
QString m_mimeType;
[[nodiscard]] bool isValid() const { return m_stream || !m_media.isEmpty(); }
void reset() { m_stream = nullptr; m_media = QUrl(); m_mimeType = QString(); }
} m_pendingJob;
int m_scanIndex;
int m_lineIndex;
bool m_utf8;
bool m_aborted;
private:
bool processLine(int startIndex, int length);
};
#define LINE_LIMIT 4096
#define READ_LIMIT 64
bool QPlaylistFileParserPrivate::processLine(int startIndex, int length)
{
Q_Q(QPlaylistFileParser);
m_lineIndex++;
if (!m_currentParser) {
const QString urlString = m_root.toString();
const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString;
QString mimeType;
if (m_source)
mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString();
m_type = QPlaylistFileParser::findPlaylistType(suffix, !mimeType.isEmpty() ? mimeType : m_mimeType, m_buffer.constData(), quint32(m_buffer.size()));
switch (m_type) {
case QPlaylistFileParser::UNKNOWN:
emit q->error(QMediaPlaylist::FormatError,
QMediaPlaylist::tr("%1 playlist type is unknown").arg(m_root.toString()));
q->abort();
return false;
case QPlaylistFileParser::M3U:
m_currentParser.reset(new M3UParser(q));
break;
case QPlaylistFileParser::M3U8:
m_currentParser.reset(new M3UParser(q));
m_utf8 = true;
break;
case QPlaylistFileParser::PLS:
m_currentParser.reset(new PLSParser(q));
break;
}
Q_ASSERT(!m_currentParser.isNull());
}
QString line;
if (m_utf8) {
line = QString::fromUtf8(m_buffer.constData() + startIndex, length).trimmed();
} else {
line = QString::fromLatin1(m_buffer.constData() + startIndex, length).trimmed();
}
if (line.isEmpty())
return true;
Q_ASSERT(m_currentParser);
return m_currentParser->parseLine(m_lineIndex, line, m_root);
}
void QPlaylistFileParserPrivate::handleData()
{
Q_Q(QPlaylistFileParser);
while (m_stream->bytesAvailable() && !m_aborted) {
int expectedBytes = qMin(READ_LIMIT, int(qMin(m_stream->bytesAvailable(),
qint64(LINE_LIMIT - m_buffer.size()))));
m_buffer.push_back(m_stream->read(expectedBytes));
int processedBytes = 0;
while (m_scanIndex < m_buffer.length() && !m_aborted) {
char s = m_buffer[m_scanIndex];
if (s == '\r' || s == '\n') {
int l = m_scanIndex - processedBytes;
if (l > 0) {
if (!processLine(processedBytes, l))
break;
}
processedBytes = m_scanIndex + 1;
if (!m_stream) {
//some error happened, so exit parsing
return;
}
}
m_scanIndex++;
}
if (m_aborted)
break;
if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
emit q->error(QMediaPlaylist::FormatError, QMediaPlaylist::tr("invalid line in playlist file"));
q->abort();
break;
}
if (!m_stream->bytesAvailable() && (!m_source || !m_source->isFinished())) {
//last line
processLine(processedBytes, -1);
break;
}
Q_ASSERT(m_buffer.length() == m_scanIndex);
if (processedBytes == 0)
continue;
int copyLength = m_buffer.length() - processedBytes;
if (copyLength > 0) {
Q_ASSERT(copyLength <= READ_LIMIT);
m_buffer = m_buffer.right(copyLength);
} else {
m_buffer.clear();
}
m_scanIndex = 0;
}
handleParserFinished();
}
QPlaylistFileParser::QPlaylistFileParser(QObject *parent)
: QObject(parent)
, d_ptr(new QPlaylistFileParserPrivate(this))
{
}
QPlaylistFileParser::~QPlaylistFileParser() = default;
QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime)
{
if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl"))
return QPlaylistFileParser::M3U;
if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl"))
return QPlaylistFileParser::M3U8;
if (mime == QLatin1String("audio/x-scpls"))
return QPlaylistFileParser::PLS;
return QPlaylistFileParser::UNKNOWN;
}
QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix)
{
const QString &s = suffix.toLower();
if (s == QLatin1String("m3u"))
return QPlaylistFileParser::M3U;
if (s == QLatin1String("m3u8"))
return QPlaylistFileParser::M3U8;
if (s == QLatin1String("pls"))
return QPlaylistFileParser::PLS;
return QPlaylistFileParser::UNKNOWN;
}
QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size)
{
if (!data || size == 0)
return QPlaylistFileParser::UNKNOWN;
if (size >= 7 && strncmp(data, "#EXTM3U", 7) == 0)
return QPlaylistFileParser::M3U;
if (size >= 10 && strncmp(data, "[playlist]", 10) == 0)
return QPlaylistFileParser::PLS;
return QPlaylistFileParser::UNKNOWN;
}
QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& suffix,
const QString& mime,
const char *data,
quint32 size)
{
FileType dataHeaderType = findByDataHeader(data, size);
if (dataHeaderType != UNKNOWN)
return dataHeaderType;
FileType mimeType = findByMimeType(mime);
if (mimeType != UNKNOWN)
return mimeType;
mimeType = findBySuffixType(mime);
if (mimeType != UNKNOWN)
return mimeType;
FileType suffixType = findBySuffixType(suffix);
if (suffixType != UNKNOWN)
return suffixType;
return UNKNOWN;
}
/*
* Delegating
*/
void QPlaylistFileParser::start(const QUrl &media, QIODevice *stream, const QString &mimeType)
{
if (stream)
start(stream, mimeType);
else
start(media, mimeType);
}
void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType)
{
Q_D(QPlaylistFileParser);
const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false;
if (!validStream) {
Q_EMIT error(QMediaPlaylist::AccessDeniedError, QMediaPlaylist::tr("Invalid stream"));
return;
}
if (!d->m_currentParser.isNull()) {
abort();
d->m_pendingJob = { stream, QUrl(), mimeType };
return;
}
playlist.clear();
d->reset();
d->m_mimeType = mimeType;
d->m_stream = stream;
connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
d->handleData();
}
void QPlaylistFileParser::start(const QUrl& request, const QString &mimeType)
{
Q_D(QPlaylistFileParser);
const QUrl &url = request.url();
if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
emit error(QMediaPlaylist::AccessDeniedError, QString(QMediaPlaylist::tr("%1 does not exist")).arg(url.toString()));
return;
}
if (!d->m_currentParser.isNull()) {
abort();
d->m_pendingJob = { nullptr, request, mimeType };
return;
}
d->reset();
d->m_root = url;
d->m_mimeType = mimeType;
d->m_source.reset(d->m_mgr.get(QNetworkRequest(request)));
d->m_stream = d->m_source.get();
connect(d->m_source.data(), SIGNAL(readyRead()), this, SLOT(handleData()));
connect(d->m_source.data(), SIGNAL(finished()), this, SLOT(handleData()));
connect(d->m_source.data(), SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(handleError()));
if (url.isLocalFile())
d->handleData();
}
void QPlaylistFileParser::abort()
{
Q_D(QPlaylistFileParser);
d->abort();
if (d->m_source)
d->m_source->disconnect();
if (d->m_stream)
disconnect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
playlist.clear();
}
void QPlaylistFileParser::handleData()
{
Q_D(QPlaylistFileParser);
d->handleData();
}
void QPlaylistFileParserPrivate::handleParserFinished()
{
Q_Q(QPlaylistFileParser);
const bool isParserValid = !m_currentParser.isNull();
if (!isParserValid && !m_aborted)
emit q->error(QMediaPlaylist::FormatNotSupportedError, QMediaPlaylist::tr("Empty file provided"));
if (isParserValid && !m_aborted) {
m_currentParser.reset();
emit q->finished();
}
if (!m_aborted)
q->abort();
if (!m_source.isNull())
m_source.reset();
if (m_pendingJob.isValid())
q->start(m_pendingJob.m_media, m_pendingJob.m_stream, m_pendingJob.m_mimeType);
}
void QPlaylistFileParserPrivate::abort()
{
m_aborted = true;
if (!m_currentParser.isNull())
m_currentParser->abort();
}
void QPlaylistFileParserPrivate::reset()
{
Q_ASSERT(m_currentParser.isNull());
Q_ASSERT(m_source.isNull());
m_buffer.clear();
m_root.clear();
m_mimeType.clear();
m_stream = nullptr;
m_type = QPlaylistFileParser::UNKNOWN;
m_scanIndex = 0;
m_lineIndex = -1;
m_utf8 = false;
m_aborted = false;
m_pendingJob.reset();
}
void QPlaylistFileParser::handleError()
{
Q_D(QPlaylistFileParser);
const QString &errorString = d->m_source->errorString();
Q_EMIT error(QMediaPlaylist::NetworkError, errorString);
abort();
}
QT_END_NAMESPACE

View File

@ -1,80 +0,0 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef PLAYLISTFILEPARSER_P_H
#define PLAYLISTFILEPARSER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qtmultimediaglobal.h"
#include "qmediaplaylist.h"
#include <QtCore/qobject.h>
QT_BEGIN_NAMESPACE
class QIODevice;
class QUrl;
class QNetworkRequest;
class QPlaylistFileParserPrivate;
class QPlaylistFileParser : public QObject
{
Q_OBJECT
public:
QPlaylistFileParser(QObject *parent = nullptr);
~QPlaylistFileParser();
enum FileType
{
UNKNOWN,
M3U,
M3U8, // UTF-8 version of M3U
PLS
};
void start(const QUrl &media, QIODevice *stream = nullptr, const QString &mimeType = QString());
void start(const QUrl &request, const QString &mimeType = QString());
void start(QIODevice *stream, const QString &mimeType = QString());
void abort();
QList<QUrl> playlist;
Q_SIGNALS:
void newItem(const QVariant& content);
void finished();
void error(QMediaPlaylist::Error err, const QString& errorMsg);
private Q_SLOTS:
void handleData();
void handleError();
private:
static FileType findByMimeType(const QString &mime);
static FileType findBySuffixType(const QString &suffix);
static FileType findByDataHeader(const char *data, quint32 size);
static FileType findPlaylistType(QIODevice *device,
const QString& mime);
static FileType findPlaylistType(const QString &suffix,
const QString& mime,
const char *data = nullptr,
quint32 size = 0);
Q_DISABLE_COPY(QPlaylistFileParser)
Q_DECLARE_PRIVATE(QPlaylistFileParser)
QScopedPointer<QPlaylistFileParserPrivate> d_ptr;
};
QT_END_NAMESPACE
#endif // PLAYLISTFILEPARSER_P_H

View File

@ -15,6 +15,8 @@
<file>icons/media-playlist-shuffle.png</file>
<file>icons/media-playlist-repeat-song.png</file>
<file>icons/media-playlist-normal.png</file>
<file>icons/media-repeat-single.png</file>
<file>icons/skin.png</file>
<file>icons/media-album-cover.svg</file>
</qresource>
</RCC>

View File

@ -1,29 +1,18 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "seekableslider.h"
SeekableSlider::SeekableSlider(QWidget *parent) :
QSlider(parent)
{
//关闭分段移动
setSingleStep(0);
setPageStep(0);
}
//点击Slider即可调节Value
//只写了横向模式的Slider……
void SeekableSlider::mousePressEvent(QMouseEvent *event)
void SeekableSlider::mouseReleaseEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
QSlider::mousePressEvent(event);
double pos = event->pos().x() / (double)width();
setValue(pos * (maximum() - minimum()) + minimum());
}
}
void SeekableSlider::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
QSlider::mousePressEvent(event);
double pos = event->pos().x() / (double)width();
setValue(pos * (maximum() - minimum()) + minimum());
}
emit sliderReleased();
return QSlider::mouseReleaseEvent(event);
}

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QSlider>
@ -8,13 +12,13 @@ class SeekableSlider : public QSlider
Q_OBJECT
public:
explicit SeekableSlider(QWidget *parent = nullptr);
~SeekableSlider() = default;
signals:
public slots:
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
};

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "singleapplicationmanager.h"
#include <QVariant>

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#ifndef SINGLEAPPLICATIONMANAGER_H
#define SINGLEAPPLICATIONMANAGER_H