Compare commits

...

4 Commits

24 changed files with 604 additions and 2801 deletions

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

@ -0,0 +1,56 @@
name: Windows CI
on: [push, pull_request, workflow_dispatch]
jobs:
msvc-cmake-build:
strategy:
matrix:
vs: ['2022']
msvc_arch: ['x64']
qt_ver: ['6.7.2']
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
:: ===== 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/
*~ # IDE folder
*.autosave .vscode/
*.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
# qtcreator generated files # User config file
*.pro.user* CMakeLists.txt.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.*

View File

@ -24,22 +24,15 @@ set (PMUSIC_CPP_FILES
main.cpp main.cpp
mainwindow.cpp mainwindow.cpp
seekableslider.cpp seekableslider.cpp
playlistmodel.cpp playlistmanager.cpp
singleapplicationmanager.cpp singleapplicationmanager.cpp
qt/qplaylistfileparser.cpp
qt/qmediaplaylist.cpp
) )
set (PMUSIC_HEADER_FILES set (PMUSIC_HEADER_FILES
mainwindow.h mainwindow.h
seekableslider.h seekableslider.h
playlistmodel.h playlistmanager.h
singleapplicationmanager.h singleapplicationmanager.h
qt/qplaylistfileparser_p.h
qt/qmediaplaylist.h
qt/qmediaplaylist_p.h
) )
set (PMUSIC_UI_FILES set (PMUSIC_UI_FILES
@ -52,19 +45,16 @@ set (EXE_NAME pmusic)
file (GLOB PMUSIC_TS_FILES languages/*.ts) file (GLOB PMUSIC_TS_FILES languages/*.ts)
set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES}) 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}
${PMUSIC_HEADER_FILES} ${PMUSIC_HEADER_FILES}
${PMUSIC_CPP_FILES} ${PMUSIC_CPP_FILES}
${PMUSIC_UI_FILES} ${PMUSIC_UI_FILES}
resources.qrc resources.qrc
)
# 3rd party code qt_add_translations(${EXE_NAME}
FlacPic.h TS_FILES
ID3v2Pic.h ${PMUSIC_TS_FILES}
${PMUSIC_QM_FILES}
) )
if (NOT TagLib_FOUND) if (NOT TagLib_FOUND)
@ -119,17 +109,3 @@ install (
TARGETS ${EXE_NAME} TARGETS ${EXE_NAME}
${INSTALL_TARGETS_DEFAULT_ARGS} ${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}
)

263
FlacPic.h
View File

@ -1,263 +0,0 @@
/*
FLAC标签图片提取库 Ver 1.0
FLAC文件中稳定便
BMPJPEGPNGGIF图片格式
使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标签中稳定便
BMPJPEGPNGGIF图片格式
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

@ -27,9 +27,7 @@ I don't have a mac, so no support at all.
## Help Translation! ## Help Translation!
[Translate this project on Transifex!](https://www.transifex.com/blumia/pineapple-music/) TODO: move to Codeberg's Weblate.
Feel free to open up an issue to request an new language to translate.
## About License ## About License
@ -39,11 +37,3 @@ Anyway here is a list of file which is in non-free state (with license: do whate
- All png images inside `icons` folder. - All png images inside `icons` folder.
- seekableslider.{h,cpp} - 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}

View File

@ -5,8 +5,8 @@ environment:
PACKAGE_INSTALL_ROOT: C:\projects\pir PACKAGE_INSTALL_ROOT: C:\projects\pir
PKG_CONFIG_PATH: C:\projects\pir\lib\pkgconfig PKG_CONFIG_PATH: C:\projects\pir\lib\pkgconfig
matrix: matrix:
- build_name: mingw1120_64_qt6_5 - build_name: mingw1120_64_qt6_7
QTPATH: C:\Qt\6.5\mingw_64 QTPATH: C:\Qt\6.7\mingw_64
MINGW64: C:\Qt\Tools\mingw1120_64 MINGW64: C:\Qt\Tools\mingw1120_64
install: install:
@ -21,11 +21,11 @@ install:
build_script: build_script:
# prepare # prepare
- mkdir 3rdparty - mkdir 3rdparty
- cinst ninja - choco install ninja
- cinst pkgconfiglite - choco install pkgconfiglite
# build taglib # build taglib
- cd 3rdparty - cd 3rdparty
- git clone -q https://github.com/taglib/taglib.git - git clone --recurse-submodules -q https://github.com/taglib/taglib.git
- cd taglib - cd taglib
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON - cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON
- cmake --build . - cmake --build .

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -4,27 +4,27 @@
<context> <context>
<name>MainWindow</name> <name>MainWindow</name>
<message> <message>
<location filename="../mainwindow.cpp" line="106"/> <location filename="../mainwindow.cpp" line="71"/>
<source>Mono</source> <source>Mono</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="108"/> <location filename="../mainwindow.cpp" line="73"/>
<source>Stereo</source> <source>Stereo</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="110"/> <location filename="../mainwindow.cpp" line="75"/>
<source>%1 Channels</source> <source>%1 Channels</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="238"/> <location filename="../mainwindow.cpp" line="200"/>
<source>Select songs to play</source> <source>Select songs to play</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="240"/> <location filename="../mainwindow.cpp" line="202"/>
<source>Audio Files</source> <source>Audio Files</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -55,54 +55,21 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="116"/> <location filename="../mainwindow.cpp" line="81"/>
<source>Sample Rate: %1 Hz</source> <source>Sample Rate: %1 Hz</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="121"/> <location filename="../mainwindow.cpp" line="86"/>
<source>Bitrate: %1 Kbps</source> <source>Bitrate: %1 Kbps</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="126"/> <location filename="../mainwindow.cpp" line="91"/>
<source>Channel Count: %1</source> <source>Channel Count: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> <context>
<name>main</name> <name>main</name>
<message> <message>

View File

@ -4,27 +4,27 @@
<context> <context>
<name>MainWindow</name> <name>MainWindow</name>
<message> <message>
<location filename="../mainwindow.cpp" line="106"/> <location filename="../mainwindow.cpp" line="71"/>
<source>Mono</source> <source>Mono</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="108"/> <location filename="../mainwindow.cpp" line="73"/>
<source>Stereo</source> <source>Stereo</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="110"/> <location filename="../mainwindow.cpp" line="75"/>
<source>%1 Channels</source> <source>%1 Channels</source>
<translation>%1 </translation> <translation>%1 </translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="238"/> <location filename="../mainwindow.cpp" line="200"/>
<source>Select songs to play</source> <source>Select songs to play</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="240"/> <location filename="../mainwindow.cpp" line="202"/>
<source>Audio Files</source> <source>Audio Files</source>
<translation></translation> <translation></translation>
</message> </message>
@ -55,54 +55,21 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="116"/> <location filename="../mainwindow.cpp" line="81"/>
<source>Sample Rate: %1 Hz</source> <source>Sample Rate: %1 Hz</source>
<translation>: %1 Hz</translation> <translation>: %1 Hz</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="121"/> <location filename="../mainwindow.cpp" line="86"/>
<source>Bitrate: %1 Kbps</source> <source>Bitrate: %1 Kbps</source>
<translation>: %1 Kbps</translation> <translation>: %1 Kbps</translation>
</message> </message>
<message> <message>
<location filename="../mainwindow.cpp" line="126"/> <location filename="../mainwindow.cpp" line="91"/>
<source>Channel Count: %1</source> <source>Channel Count: %1</source>
<translation>: %1</translation> <translation>: %1</translation>
</message> </message>
</context> </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> <context>
<name>main</name> <name>main</name>
<message> <message>

View File

@ -14,13 +14,9 @@ int main(int argc, char *argv[])
QApplication a(argc, argv); QApplication a(argc, argv);
QTranslator translator; QTranslator translator;
QString qmDir; if (translator.load(QLocale(), QLatin1String("pineapple-music"), QLatin1String("_"), QLatin1String(":/i18n"))) {
#ifdef _WIN32 a.installTranslator(&translator);
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);
a.installTranslator(&translator); a.installTranslator(&translator);
// parse commandline arguments // parse commandline arguments

View File

@ -1,11 +1,7 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "./ui_mainwindow.h" #include "./ui_mainwindow.h"
#include "playlistmodel.h" #include "playlistmanager.h"
#include "qt/qmediaplaylist.h"
#include "ID3v2Pic.h"
#include "FlacPic.h"
// taglib // taglib
#ifndef NO_TAGLIB #ifndef NO_TAGLIB
@ -14,6 +10,7 @@
#include <QPainter> #include <QPainter>
#include <QMediaPlayer> #include <QMediaPlayer>
#include <QMediaMetaData>
#include <QAudioOutput> #include <QAudioOutput>
#include <QPropertyAnimation> #include <QPropertyAnimation>
#include <QFileDialog> #include <QFileDialog>
@ -30,11 +27,16 @@ MainWindow::MainWindow(QWidget *parent)
, ui(new Ui::MainWindow) , ui(new Ui::MainWindow)
, m_mediaPlayer(new QMediaPlayer(this)) , m_mediaPlayer(new QMediaPlayer(this))
, m_audioOutput(new QAudioOutput(this)) , m_audioOutput(new QAudioOutput(this))
, m_playlistModel(new PlaylistModel(this)) , m_playlistManager(new PlaylistManager(this))
{ {
ui->setupUi(this); ui->setupUi(this);
m_playlistManager->setAutoLoadFilterSuffixes({
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga"
});
m_mediaPlayer->setAudioOutput(m_audioOutput); 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->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
this->setAttribute(Qt::WA_TranslucentBackground, true); this->setAttribute(Qt::WA_TranslucentBackground, true);
@ -55,45 +57,12 @@ void MainWindow::commandlinePlayAudioFiles(QStringList audioFiles)
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles); QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
if (!audioFileUrls.isEmpty()) { if (!audioFileUrls.isEmpty()) {
if (audioFileUrls.count() == 1) { QModelIndex modelIndex = m_playlistManager->loadPlaylist(audioFileUrls);
loadPlaylistBySingleLocalFile(audioFileUrls.first().toLocalFile()); if (modelIndex.isValid()) {
} else { m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
createPlaylist(audioFileUrls); play();
}
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;
} }
} }
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) void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt)
@ -193,7 +162,6 @@ void MainWindow::mousePressEvent(QMouseEvent *event)
void MainWindow::mouseMoveEvent(QMouseEvent *event) void MainWindow::mouseMoveEvent(QMouseEvent *event)
{ {
if (event->buttons() & Qt::LeftButton && m_clickedOnWindow) { if (event->buttons() & Qt::LeftButton && m_clickedOnWindow) {
qDebug() << "??" << event << event->flags() << event->isBeginEvent() << event->isEndEvent();
window()->windowHandle()->startSystemMove(); window()->windowHandle()->startSystemMove();
event->accept(); event->accept();
} }
@ -204,7 +172,6 @@ void MainWindow::mouseMoveEvent(QMouseEvent *event)
void MainWindow::mouseReleaseEvent(QMouseEvent *event) void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{ {
m_clickedOnWindow = false; m_clickedOnWindow = false;
qDebug() << "?";
return QMainWindow::mouseReleaseEvent(event); return QMainWindow::mouseReleaseEvent(event);
} }
@ -227,10 +194,11 @@ void MainWindow::dropEvent(QDropEvent *e)
return; return;
} }
// TODO: file/format filter? QModelIndex modelIndex = m_playlistManager->loadPlaylist(urls);
if (modelIndex.isValid()) {
createPlaylist(urls); m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
m_mediaPlayer->play(); play();
}
} }
void MainWindow::loadFile() void MainWindow::loadFile()
@ -244,39 +212,33 @@ void MainWindow::loadFile()
urlList.append(QUrl::fromLocalFile(fileName)); urlList.append(QUrl::fromLocalFile(fileName));
} }
createPlaylist(urlList); m_playlistManager->loadPlaylist(urlList);
m_mediaPlayer->setSource(urlList.first());
} }
/* void MainWindow::play()
* The returned QMediaPlaylist* ownership belongs to the internal QMediaPlayer instance.
*/
void MainWindow::createPlaylist(QList<QUrl> urlList, int index)
{ {
QMediaPlaylist* playlist = m_playlistModel->playlist(); QUrl fileUrl(m_mediaPlayer->source());
playlist->clear();
playlist->addMedia(urlList);
connect(playlist, &QMediaPlaylist::playbackModeChanged, this, [=](QMediaPlaylist::PlaybackMode mode) { m_mediaPlayer->play();
switch (mode) {
case QMediaPlaylist::CurrentItemInLoop: ui->titleLabel->setText(fileUrl.fileName());
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png")); ui->titleLabel->setToolTip(fileUrl.fileName());
break;
case QMediaPlaylist::Loop: if (fileUrl.isLocalFile()) {
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png")); QString filePath(fileUrl.toLocalFile());
break; QString suffix(filePath.mid(filePath.lastIndexOf('.') + 1));
case QMediaPlaylist::Sequential: suffix = suffix.toUpper();
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-normal.png"));
break; #ifndef NO_TAGLIB
// case QMediaPlaylist::Random: TagLib::FileRef fileRef(filePath.toLocal8Bit().data());
// ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png"));
// break; if (!fileRef.isNull() && fileRef.audioProperties()) {
default: TagLib::AudioProperties *prop = fileRef.audioProperties();
break; setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
}
#endif // NO_TAGLIB
} }
});
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
playlist->setCurrentIndex(index < 0 ? 0 : index);
} }
void MainWindow::centerWindow() void MainWindow::centerWindow()
@ -300,7 +262,7 @@ void MainWindow::on_playBtn_clicked()
{ {
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) { if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
loadFile(); loadFile();
m_mediaPlayer->play(); play();
} else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) { } else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) {
ui->propLabel->setText("Error: InvalidMedia" + m_mediaPlayer->errorString()); ui->propLabel->setText("Error: InvalidMedia" + m_mediaPlayer->errorString());
} else { } else {
@ -359,27 +321,18 @@ void MainWindow::on_playbackSlider_valueChanged(int value)
void MainWindow::on_prevBtn_clicked() void MainWindow::on_prevBtn_clicked()
{ {
// QMediaPlaylist::previous() won't work when in CurrentItemInLoop playmode, QModelIndex index(m_playlistManager->previousIndex());
// and also works not as intended when in other playmode, so do it manually... m_playlistManager->setCurrentIndex(index);
QMediaPlaylist * playlist = m_playlistModel->playlist(); m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
if (playlist) { play();
int index = playlist->currentIndex();
int count = playlist->mediaCount();
playlist->setCurrentIndex(index == 0 ? count - 1 : index - 1);
}
} }
void MainWindow::on_nextBtn_clicked() void MainWindow::on_nextBtn_clicked()
{ {
// see also: MainWindow::on_prevBtn_clicked() QModelIndex index(m_playlistManager->nextIndex());
QMediaPlaylist * playlist = m_playlistModel->playlist(); m_playlistManager->setCurrentIndex(index);
if (playlist) { m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
int index = playlist->currentIndex(); play();
int count = playlist->mediaCount();
playlist->setCurrentIndex(index == (count - 1) ? 0 : index + 1);
}
} }
void MainWindow::on_volumeBtn_clicked() void MainWindow::on_volumeBtn_clicked()
@ -404,71 +357,26 @@ void MainWindow::initUiAndAnimation()
m_fadeOutAnimation->setStartValue(1); m_fadeOutAnimation->setStartValue(1);
m_fadeOutAnimation->setEndValue(0); m_fadeOutAnimation->setEndValue(0);
connect(m_fadeOutAnimation, &QPropertyAnimation::finished, this, &QMainWindow::close); connect(m_fadeOutAnimation, &QPropertyAnimation::finished, this, &QMainWindow::close);
setFixedSize(490, 160);
// 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.
} }
void MainWindow::initConnections() void MainWindow::initConnections()
{ {
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentIndexChanged, this, [=](int currentItem) { connect(m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, [=](){
bool isPlaying = m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState; QMediaMetaData metadata(m_mediaPlayer->metaData());
m_mediaPlayer->setSource(m_playlistModel->playlist()->currentMedia()); setAudioMetadataForDisplay(metadata.stringValue(QMediaMetaData::Title),
if (isPlaying) m_mediaPlayer->play(); metadata.stringValue(QMediaMetaData::Author),
}); metadata.stringValue(QMediaMetaData::AlbumTitle));
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentMediaChanged, this, [=](const QUrl &fileUrl) { QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
ui->titleLabel->setText(fileUrl.fileName()); if (!coverArt.isNull()) {
ui->titleLabel->setToolTip(fileUrl.fileName()); ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
} else {
if (fileUrl.isLocalFile()) { qDebug() << "No ThumbnailImage!" << metadata.keys();
QString filePath(fileUrl.toLocalFile());
QString suffix(filePath.mid(filePath.lastIndexOf('.') + 1));
suffix = suffix.toUpper();
#ifndef NO_TAGLIB
TagLib::FileRef fileRef(filePath.toLocal8Bit().data());
if (!fileRef.isNull() && fileRef.audioProperties()) {
TagLib::AudioProperties *prop = fileRef.audioProperties();
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
}
if (!fileRef.isNull() && fileRef.tag()) {
TagLib::Tag * tag = fileRef.tag();
setAudioMetadataForDisplay(QString::fromStdString(tag->title().to8Bit(true)),
QString::fromStdString(tag->artist().to8Bit(true)),
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) {
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg")); 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) { connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
@ -506,35 +414,71 @@ void MainWindow::initConnections()
ui->volumeSlider->setValue(vol * 100); ui->volumeSlider->setValue(vol * 100);
}); });
// connect(m_mediaPlayer, static_cast<void(QMediaPlayer::*)(QMediaPlayer::Error)>(&QMediaPlayer::error), connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, [=](QMediaPlayer::MediaStatus status){
// this, [=](QMediaPlayer::Error error) { if (status == QMediaPlayer::EndOfMedia) {
// switch (error) { switch (m_playbackMode) {
// default: case MainWindow::CurrentItemOnce:
// break; // do nothing
// } break;
// qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data()); 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::on_playbackModeBtn_clicked() void MainWindow::on_playbackModeBtn_clicked()
{ {
QMediaPlaylist * playlist = m_playlistModel->playlist(); switch (m_playbackMode) {
if (!playlist) return; case MainWindow::CurrentItemOnce:
setProperty("playbackMode", MainWindow::CurrentItemInLoop);
switch (playlist->playbackMode()) {
case QMediaPlaylist::CurrentItemInLoop:
playlist->setPlaybackMode(QMediaPlaylist::Loop);
break; break;
case QMediaPlaylist::Loop: case MainWindow::CurrentItemInLoop:
playlist->setPlaybackMode(QMediaPlaylist::Sequential); setProperty("playbackMode", MainWindow::Sequential);
break; break;
case QMediaPlaylist::Sequential: case MainWindow::Sequential:
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop); setProperty("playbackMode", MainWindow::CurrentItemOnce);
break; break;
// case QMediaPlaylist::Random:
// playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
// break;
default: default:
break; break;
} }
} }
void MainWindow::on_playListBtn_clicked()
{
setFixedHeight(size().height() < 200 ? 420 : 160);
}
void MainWindow::on_playlistView_activated(const QModelIndex &index)
{
m_playlistManager->setCurrentIndex(index);
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
play();
}

View File

@ -12,17 +12,25 @@ class QAudioOutput;
class QPropertyAnimation; class QPropertyAnimation;
QT_END_NAMESPACE QT_END_NAMESPACE
class PlaylistModel; class PlaylistManager;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
Q_OBJECT Q_OBJECT
public: public:
enum PlaybackMode {
CurrentItemOnce,
CurrentItemInLoop,
Sequential,
};
Q_ENUM(PlaybackMode)
Q_PROPERTY(PlaybackMode playbackMode MEMBER m_playbackMode NOTIFY playbackModeChanged)
MainWindow(QWidget *parent = nullptr); MainWindow(QWidget *parent = nullptr);
~MainWindow() override; ~MainWindow() override;
void commandlinePlayAudioFiles(QStringList audioFiles); void commandlinePlayAudioFiles(QStringList audioFiles);
void loadPlaylistBySingleLocalFile(const QString &path);
void setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt); void setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt);
void setAudioMetadataForDisplay(QString title, QString artist, QString album); void setAudioMetadataForDisplay(QString title, QString artist, QString album);
@ -39,8 +47,9 @@ protected:
void dropEvent(QDropEvent *e) override; void dropEvent(QDropEvent *e) override;
void loadFile(); void loadFile();
void play();
void centerWindow(); void centerWindow();
void createPlaylist(QList<QUrl> urlList, int index = -1);
private slots: private slots:
void on_playbackModeBtn_clicked(); void on_playbackModeBtn_clicked();
@ -54,17 +63,25 @@ private slots:
void on_volumeBtn_clicked(); void on_volumeBtn_clicked();
void on_minimumWindowBtn_clicked(); void on_minimumWindowBtn_clicked();
void on_playListBtn_clicked();
void on_playlistView_activated(const QModelIndex &index);
signals:
void playbackModeChanged(enum PlaybackMode mode);
private: private:
bool m_clickedOnWindow = false; bool m_clickedOnWindow = false;
bool m_playbackSliderPressed = false; bool m_playbackSliderPressed = false;
QLinearGradient m_bgLinearGradient; QLinearGradient m_bgLinearGradient;
enum PlaybackMode m_playbackMode = CurrentItemInLoop;
Ui::MainWindow *ui; Ui::MainWindow *ui;
QMediaPlayer *m_mediaPlayer; QMediaPlayer *m_mediaPlayer;
QAudioOutput *m_audioOutput; QAudioOutput *m_audioOutput;
QPropertyAnimation *m_fadeOutAnimation; QPropertyAnimation *m_fadeOutAnimation;
PlaylistModel *m_playlistModel = nullptr; // TODO: move playback logic to player.cpp PlaylistManager *m_playlistManager;
void initUiAndAnimation(); void initUiAndAnimation();
void initConnections(); void initConnections();

View File

@ -209,7 +209,7 @@ QLabel#coverLabel {
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@ -277,7 +277,7 @@ QLabel#coverLabel {
<item> <item>
<layout class="QVBoxLayout" name="playerPanelLayout"> <layout class="QVBoxLayout" name="playerPanelLayout">
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum> <enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>10</number> <number>10</number>
@ -320,7 +320,7 @@ QLabel#coverLabel {
<string>0:00</string> <string>0:00</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property> </property>
</widget> </widget>
</item> </item>
@ -332,7 +332,7 @@ QLabel#coverLabel {
<number>1000</number> <number>1000</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@ -562,7 +562,7 @@ QLabel#coverLabel {
<number>100</number> <number>100</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@ -576,9 +576,9 @@ QLabel#coverLabel {
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QWidget" name="pluginWidget" native="true"> <widget class="QStackedWidget" name="pluginStackedWidget">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy hsizetype="Ignored" vsizetype="Ignored">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
@ -589,6 +589,28 @@ QLabel#coverLabel {
<height>0</height> <height>0</height>
</size> </size>
</property> </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> </widget>
</item> </item>
</layout> </layout>

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,7 @@
<file>icons/media-playlist-shuffle.png</file> <file>icons/media-playlist-shuffle.png</file>
<file>icons/media-playlist-repeat-song.png</file> <file>icons/media-playlist-repeat-song.png</file>
<file>icons/media-playlist-normal.png</file> <file>icons/media-playlist-normal.png</file>
<file>icons/media-repeat-single.png</file>
<file>icons/media-album-cover.svg</file> <file>icons/media-album-cover.svg</file>
</qresource> </qresource>
</RCC> </RCC>