Compare commits
No commits in common. "d28108f2e5a50d0f11c6156560af674f988ec85a" and "8aa68ccebda9302324eec83d1c068bbdba749b22" have entirely different histories.
d28108f2e5
...
8aa68ccebd
56
.github/workflows/windows.yml
vendored
56
.github/workflows/windows.yml
vendored
|
@ -1,56 +0,0 @@
|
||||||
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
81
.gitignore
vendored
|
@ -1,8 +1,77 @@
|
||||||
# Common build folder
|
# This file is used to ignore files which are generated
|
||||||
[Bb]uild/
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
# IDE folder
|
*~
|
||||||
.vscode/
|
*.autosave
|
||||||
|
*.a
|
||||||
|
*.core
|
||||||
|
*.moc
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*_pch.h.cpp
|
||||||
|
*_resource.rc
|
||||||
|
*.qm
|
||||||
|
.#*
|
||||||
|
*.*#
|
||||||
|
core
|
||||||
|
!core/
|
||||||
|
tags
|
||||||
|
.DS_Store
|
||||||
|
.directory
|
||||||
|
*.debug
|
||||||
|
Makefile*
|
||||||
|
*.prl
|
||||||
|
*.app
|
||||||
|
moc_*.cpp
|
||||||
|
ui_*.h
|
||||||
|
qrc_*.cpp
|
||||||
|
Thumbs.db
|
||||||
|
*.res
|
||||||
|
*.rc
|
||||||
|
/.qmake.cache
|
||||||
|
/.qmake.stash
|
||||||
|
|
||||||
# User config file
|
# qtcreator generated files
|
||||||
CMakeLists.txt.user*
|
*.pro.user*
|
||||||
|
|
||||||
|
# xemacs temporary files
|
||||||
|
*.flc
|
||||||
|
|
||||||
|
# Vim temporary files
|
||||||
|
.*.swp
|
||||||
|
|
||||||
|
# Visual Studio generated files
|
||||||
|
*.ib_pdb_index
|
||||||
|
*.idb
|
||||||
|
*.ilk
|
||||||
|
*.pdb
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.vcproj
|
||||||
|
*vcproj.*.*.user
|
||||||
|
*.ncb
|
||||||
|
*.sdf
|
||||||
|
*.opensdf
|
||||||
|
*.vcxproj
|
||||||
|
*vcxproj.*
|
||||||
|
|
||||||
|
# MinGW generated files
|
||||||
|
*.Debug
|
||||||
|
*.Release
|
||||||
|
|
||||||
|
# Python byte code
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Binaries
|
||||||
|
# --------
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# User Config
|
||||||
|
# -----------
|
||||||
|
*.user
|
||||||
|
*.user.*
|
||||||
|
|
|
@ -24,15 +24,22 @@ set (PMUSIC_CPP_FILES
|
||||||
main.cpp
|
main.cpp
|
||||||
mainwindow.cpp
|
mainwindow.cpp
|
||||||
seekableslider.cpp
|
seekableslider.cpp
|
||||||
playlistmanager.cpp
|
playlistmodel.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
|
||||||
playlistmanager.h
|
playlistmodel.h
|
||||||
singleapplicationmanager.h
|
singleapplicationmanager.h
|
||||||
|
|
||||||
|
qt/qplaylistfileparser_p.h
|
||||||
|
qt/qmediaplaylist.h
|
||||||
|
qt/qmediaplaylist_p.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_UI_FILES
|
set (PMUSIC_UI_FILES
|
||||||
|
@ -45,16 +52,19 @@ 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
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_translations(${EXE_NAME}
|
# 3rd party code
|
||||||
TS_FILES
|
FlacPic.h
|
||||||
${PMUSIC_TS_FILES}
|
ID3v2Pic.h
|
||||||
|
|
||||||
|
${PMUSIC_QM_FILES}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (NOT TagLib_FOUND)
|
if (NOT TagLib_FOUND)
|
||||||
|
@ -109,3 +119,17 @@ 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
Executable file
263
FlacPic.h
Executable file
|
@ -0,0 +1,263 @@
|
||||||
|
/*
|
||||||
|
FLAC标签图片提取库 Ver 1.0
|
||||||
|
从FLAC文件中稳定、快捷、高效、便捷地提取出图片数据
|
||||||
|
支持BMP、JPEG、PNG、GIF图片格式
|
||||||
|
可将图片数据提取到文件或内存中,并能安全地释放内存
|
||||||
|
使用方式与ID3v2版本相同
|
||||||
|
ShadowPower 于2014/8/1 夜间
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ShadowPower_FLACPIC___
|
||||||
|
#define _ShadowPower_FLACPIC___
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#ifndef NULL
|
||||||
|
#define NULL 0
|
||||||
|
#endif
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
typedef unsigned char byte;
|
||||||
|
|
||||||
|
namespace spFLAC {
|
||||||
|
//Flac元数据块头部结构体定义
|
||||||
|
struct FlacMetadataBlockHeader
|
||||||
|
{
|
||||||
|
byte flag; //标志位,高1位:是否为最后一个数据块,低7位:数据块类型
|
||||||
|
byte length[3]; //数据块长度,不含数据块头部
|
||||||
|
};
|
||||||
|
|
||||||
|
byte *pPicData = 0; //指向图片数据的指针
|
||||||
|
int picLength = 0; //存放图片数据长度
|
||||||
|
char picFormat[4] = {}; //存放图片数据的格式(扩展名)
|
||||||
|
|
||||||
|
//检测图片格式,参数1:数据,返回值:是否成功(不是图片则失败)
|
||||||
|
bool verificationPictureFormat(char *data)
|
||||||
|
{
|
||||||
|
//支持格式:JPEG/PNG/BMP/GIF
|
||||||
|
byte jpeg[2] = { 0xff, 0xd8 };
|
||||||
|
byte png[8] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
|
||||||
|
byte gif[6] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
|
||||||
|
byte gif2[6] = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 };
|
||||||
|
byte bmp[2] = { 0x42, 0x4d };
|
||||||
|
memset(&picFormat, 0, 4);
|
||||||
|
if (memcmp(data, &jpeg, 2) == 0)
|
||||||
|
{
|
||||||
|
strcpy(picFormat, "jpg");
|
||||||
|
}
|
||||||
|
else if (memcmp(data, &png, 8) == 0)
|
||||||
|
{
|
||||||
|
strcpy(picFormat, "png");
|
||||||
|
}
|
||||||
|
else if (memcmp(data, &gif, 6) == 0 || memcmp(data, &gif2, 6) == 0)
|
||||||
|
{
|
||||||
|
strcpy(picFormat, "gif");
|
||||||
|
}
|
||||||
|
else if (memcmp(data, &bmp, 2) == 0)
|
||||||
|
{
|
||||||
|
strcpy(picFormat, "bmp");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//安全释放内存
|
||||||
|
void freePictureData()
|
||||||
|
{
|
||||||
|
if (pPicData)
|
||||||
|
{
|
||||||
|
delete pPicData;
|
||||||
|
}
|
||||||
|
pPicData = 0;
|
||||||
|
picLength = 0;
|
||||||
|
memset(&picFormat, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
//将图片提取到内存,参数1:文件路径,成功返回true
|
||||||
|
bool loadPictureData(const char *inFilePath)
|
||||||
|
{
|
||||||
|
freePictureData();
|
||||||
|
FILE *fp = NULL;
|
||||||
|
fp = fopen(inFilePath, "rb");
|
||||||
|
if (!fp) //如果打开失败
|
||||||
|
{
|
||||||
|
fp = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fseek(fp, 0, SEEK_SET); //设文件流指针到文件头部
|
||||||
|
byte magic[4] = {}; //存放校验数据
|
||||||
|
memset(&magic, 0, 4);
|
||||||
|
fread(&magic, 4, 1, fp); //读入校验数据
|
||||||
|
byte fLaC[4] = { 0x66, 0x4c, 0x61, 0x43 };
|
||||||
|
if (memcmp(&magic, &fLaC, 4) == 0)
|
||||||
|
{
|
||||||
|
//数据校验正确,文件类型为Flac
|
||||||
|
FlacMetadataBlockHeader fmbh; //创建Flac元数据块头部结构体
|
||||||
|
memset(&fmbh, 0, 4); //清空内存
|
||||||
|
fread(&fmbh, 4, 1, fp); //读入头部数据
|
||||||
|
//计算数据块长度,不含头部
|
||||||
|
int blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];
|
||||||
|
int loopCount = 0; //循环计数,防死
|
||||||
|
while ((fmbh.flag & 0x7f) != 6)
|
||||||
|
{
|
||||||
|
//如果数据类型不是图片,此处循环执行
|
||||||
|
loopCount++;
|
||||||
|
if (loopCount > 40)
|
||||||
|
{
|
||||||
|
//循环40次没有遇到末尾就直接停止
|
||||||
|
fclose(fp);
|
||||||
|
fp = NULL;
|
||||||
|
return false; //可能文件不正常
|
||||||
|
}
|
||||||
|
fseek(fp, blockLength, SEEK_CUR); //跳过数据块
|
||||||
|
if ((fmbh.flag & 0x80) == 0x80)
|
||||||
|
{
|
||||||
|
//已经是最后一个数据块了,仍然不是图片
|
||||||
|
fclose(fp);
|
||||||
|
fp = NULL;
|
||||||
|
return false; //没有找到图片数据
|
||||||
|
}
|
||||||
|
//取得下一数据块头部
|
||||||
|
memset(&fmbh, 0, 4); //清空内存
|
||||||
|
fread(&fmbh, 4, 1, fp); //读入头部数据
|
||||||
|
blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];//计算数据块长度
|
||||||
|
}
|
||||||
|
//此时已到图片数据块
|
||||||
|
|
||||||
|
int nonPicDataLength = 0; //非图片数据长度
|
||||||
|
fseek(fp, 4, SEEK_CUR); //信仰之跃
|
||||||
|
nonPicDataLength += 4;
|
||||||
|
char nextJumpLength[4]; //下次要跳的长度
|
||||||
|
fread(&nextJumpLength, 4, 1, fp); //读取安全跳跃距离
|
||||||
|
nonPicDataLength += 4;
|
||||||
|
int jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];//计算数据块长度
|
||||||
|
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump!!
|
||||||
|
nonPicDataLength += jumpLength;
|
||||||
|
fread(&nextJumpLength, 4, 1, fp);
|
||||||
|
nonPicDataLength += 4;
|
||||||
|
jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];
|
||||||
|
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump too!!
|
||||||
|
nonPicDataLength += jumpLength;
|
||||||
|
fseek(fp, 20, SEEK_CUR); //信仰之跃
|
||||||
|
nonPicDataLength += 20;
|
||||||
|
|
||||||
|
//非主流情况检测+获得文件格式
|
||||||
|
char tempData[20] = {};
|
||||||
|
memset(tempData, 0, 20);
|
||||||
|
fread(&tempData, 8, 1, fp);
|
||||||
|
fseek(fp, -8, SEEK_CUR); //回到原位
|
||||||
|
//判断40次,一位一位跳到文件头
|
||||||
|
bool ok = false; //是否正确识别出文件头
|
||||||
|
for (int i = 0; i < 40; i++)
|
||||||
|
{
|
||||||
|
//校验文件头
|
||||||
|
if (verificationPictureFormat(tempData))
|
||||||
|
{
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//如果校验失败尝试继续向后校验
|
||||||
|
fseek(fp, 1, SEEK_CUR);
|
||||||
|
nonPicDataLength++;
|
||||||
|
fread(&tempData, 8, 1, fp);
|
||||||
|
fseek(fp, -8, SEEK_CUR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
fclose(fp);
|
||||||
|
fp = NULL;
|
||||||
|
freePictureData();
|
||||||
|
return false; //无法识别的数据
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----抵达图片数据区-----
|
||||||
|
picLength = blockLength - nonPicDataLength; //计算图片数据长度
|
||||||
|
pPicData = new byte[picLength]; //动态分配图片数据内存空间
|
||||||
|
memset(pPicData, 0, picLength); //清空图片数据内存
|
||||||
|
fread(pPicData, picLength, 1, fp); //得到图片数据
|
||||||
|
//------------------------
|
||||||
|
fclose(fp); //操作已完成,关闭文件。
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//校验失败,不是Flac
|
||||||
|
fclose(fp);
|
||||||
|
fp = NULL;
|
||||||
|
freePictureData();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//取得图片数据的长度
|
||||||
|
int getPictureLength()
|
||||||
|
{
|
||||||
|
return picLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
//取得指向图片数据的指针
|
||||||
|
byte *getPictureDataPtr()
|
||||||
|
{
|
||||||
|
return pPicData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//取得图片数据的扩展名(指针)
|
||||||
|
char *getPictureFormat()
|
||||||
|
{
|
||||||
|
return picFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writePictureDataToFile(const char *outFilePath)
|
||||||
|
{
|
||||||
|
FILE *fp = NULL;
|
||||||
|
if (picLength > 0)
|
||||||
|
{
|
||||||
|
fp = fopen(outFilePath, "wb"); //打开目标文件
|
||||||
|
if (fp) //打开成功
|
||||||
|
{
|
||||||
|
fwrite(pPicData, picLength, 1, fp); //写入文件
|
||||||
|
fclose(fp); //关闭
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false; //文件打开失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false; //没有图像数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//提取图片文件,参数1:输入文件,参数2:输出文件,返回值:是否成功
|
||||||
|
bool extractPicture(const char *inFilePath, const char *outFilePath)
|
||||||
|
{
|
||||||
|
if (loadPictureData(inFilePath)) //如果取得图片数据成功
|
||||||
|
{
|
||||||
|
if (writePictureDataToFile(outFilePath))
|
||||||
|
{
|
||||||
|
return true; //文件写出成功
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false; //文件写出失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false; //无图片数据
|
||||||
|
}
|
||||||
|
freePictureData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
441
ID3v2Pic.h
Executable file
441
ID3v2Pic.h
Executable file
|
@ -0,0 +1,441 @@
|
||||||
|
/*
|
||||||
|
ID3v2标签图片提取库 Ver 1.0
|
||||||
|
支持ID3v2所有版本
|
||||||
|
从ID3v2标签中稳定、快捷、高效、便捷地提取出图片数据
|
||||||
|
支持BMP、JPEG、PNG、GIF图片格式
|
||||||
|
可将图片数据提取到文件或内存中,并能安全地释放内存
|
||||||
|
ShadowPower 于2014/8/1 上午
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ShadowPower_ID3V2PIC___
|
||||||
|
#define _ShadowPower_ID3V2PIC___
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#ifndef NULL
|
||||||
|
#define NULL 0
|
||||||
|
#endif
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
typedef unsigned char byte;
|
||||||
|
|
||||||
|
namespace spID3 {
|
||||||
|
//ID3v2标签头部结构体定义
|
||||||
|
struct ID3V2Header
|
||||||
|
{
|
||||||
|
char identi[3];//ID3头部校验,必须为“ID3”否则认为不存在ID3标签
|
||||||
|
byte major; //ID3版本号,3是ID3v2.3,4是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); //内存填0,10个字节
|
||||||
|
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
|
12
README.md
12
README.md
|
@ -27,7 +27,9 @@ I don't have a mac, so no support at all.
|
||||||
|
|
||||||
## Help Translation!
|
## Help Translation!
|
||||||
|
|
||||||
TODO: move to Codeberg's Weblate.
|
[Translate this project on Transifex!](https://www.transifex.com/blumia/pineapple-music/)
|
||||||
|
|
||||||
|
Feel free to open up an issue to request an new language to translate.
|
||||||
|
|
||||||
## About License
|
## About License
|
||||||
|
|
||||||
|
@ -37,3 +39,11 @@ 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}
|
||||||
|
|
10
appveyor.yml
10
appveyor.yml
|
@ -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_7
|
- build_name: mingw1120_64_qt6_5
|
||||||
QTPATH: C:\Qt\6.7\mingw_64
|
QTPATH: C:\Qt\6.5\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
|
||||||
- choco install ninja
|
- cinst ninja
|
||||||
- choco install pkgconfiglite
|
- cinst pkgconfiglite
|
||||||
# build taglib
|
# build taglib
|
||||||
- cd 3rdparty
|
- cd 3rdparty
|
||||||
- git clone --recurse-submodules -q https://github.com/taglib/taglib.git
|
- git clone -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.
Before Width: | Height: | Size: 18 KiB |
|
@ -4,27 +4,27 @@
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="71"/>
|
<location filename="../mainwindow.cpp" line="106"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="73"/>
|
<location filename="../mainwindow.cpp" line="108"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="75"/>
|
<location filename="../mainwindow.cpp" line="110"/>
|
||||||
<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="200"/>
|
<location filename="../mainwindow.cpp" line="238"/>
|
||||||
<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="202"/>
|
<location filename="../mainwindow.cpp" line="240"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
@ -55,21 +55,54 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="81"/>
|
<location filename="../mainwindow.cpp" line="116"/>
|
||||||
<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="86"/>
|
<location filename="../mainwindow.cpp" line="121"/>
|
||||||
<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="91"/>
|
<location filename="../mainwindow.cpp" line="126"/>
|
||||||
<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>
|
||||||
|
|
|
@ -4,27 +4,27 @@
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="71"/>
|
<location filename="../mainwindow.cpp" line="106"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation>单声道</translation>
|
<translation>单声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="73"/>
|
<location filename="../mainwindow.cpp" line="108"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation>立体声</translation>
|
<translation>立体声</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="75"/>
|
<location filename="../mainwindow.cpp" line="110"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation>%1 声道</translation>
|
<translation>%1 声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="200"/>
|
<location filename="../mainwindow.cpp" line="238"/>
|
||||||
<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="202"/>
|
<location filename="../mainwindow.cpp" line="240"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation>音频文件</translation>
|
<translation>音频文件</translation>
|
||||||
</message>
|
</message>
|
||||||
|
@ -55,21 +55,54 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="81"/>
|
<location filename="../mainwindow.cpp" line="116"/>
|
||||||
<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="86"/>
|
<location filename="../mainwindow.cpp" line="121"/>
|
||||||
<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="91"/>
|
<location filename="../mainwindow.cpp" line="126"/>
|
||||||
<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>
|
||||||
|
|
10
main.cpp
10
main.cpp
|
@ -14,9 +14,13 @@ int main(int argc, char *argv[])
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
if (translator.load(QLocale(), QLatin1String("pineapple-music"), QLatin1String("_"), QLatin1String(":/i18n"))) {
|
QString qmDir;
|
||||||
a.installTranslator(&translator);
|
#ifdef _WIN32
|
||||||
}
|
qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations");
|
||||||
|
#else
|
||||||
|
qmDir = QT_STRINGIFY(QM_FILE_INSTALL_DIR);
|
||||||
|
#endif
|
||||||
|
translator.load(QString("pineapple-music_%1").arg(QLocale::system().name()), qmDir);
|
||||||
a.installTranslator(&translator);
|
a.installTranslator(&translator);
|
||||||
|
|
||||||
// parse commandline arguments
|
// parse commandline arguments
|
||||||
|
|
294
mainwindow.cpp
294
mainwindow.cpp
|
@ -1,7 +1,11 @@
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "./ui_mainwindow.h"
|
#include "./ui_mainwindow.h"
|
||||||
|
|
||||||
#include "playlistmanager.h"
|
#include "playlistmodel.h"
|
||||||
|
#include "qt/qmediaplaylist.h"
|
||||||
|
|
||||||
|
#include "ID3v2Pic.h"
|
||||||
|
#include "FlacPic.h"
|
||||||
|
|
||||||
// taglib
|
// taglib
|
||||||
#ifndef NO_TAGLIB
|
#ifndef NO_TAGLIB
|
||||||
|
@ -10,7 +14,6 @@
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
#include <QMediaMetaData>
|
|
||||||
#include <QAudioOutput>
|
#include <QAudioOutput>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
@ -27,16 +30,11 @@ 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_playlistManager(new PlaylistManager(this))
|
, m_playlistModel(new PlaylistModel(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);
|
||||||
|
@ -57,12 +55,45 @@ void MainWindow::commandlinePlayAudioFiles(QStringList audioFiles)
|
||||||
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
|
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
|
||||||
|
|
||||||
if (!audioFileUrls.isEmpty()) {
|
if (!audioFileUrls.isEmpty()) {
|
||||||
QModelIndex modelIndex = m_playlistManager->loadPlaylist(audioFileUrls);
|
if (audioFileUrls.count() == 1) {
|
||||||
if (modelIndex.isValid()) {
|
loadPlaylistBySingleLocalFile(audioFileUrls.first().toLocalFile());
|
||||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
|
} else {
|
||||||
play();
|
createPlaylist(audioFileUrls);
|
||||||
|
}
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadPlaylistBySingleLocalFile(const QString &path)
|
||||||
|
{
|
||||||
|
QFileInfo info(path);
|
||||||
|
QDir dir(info.path());
|
||||||
|
QString currentFileName = info.fileName();
|
||||||
|
QStringList entryList = dir.entryList({"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga"},
|
||||||
|
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
||||||
|
|
||||||
|
QCollator collator;
|
||||||
|
collator.setNumericMode(true);
|
||||||
|
|
||||||
|
std::sort(entryList.begin(), entryList.end(), collator);
|
||||||
|
|
||||||
|
QList<QUrl> urlList;
|
||||||
|
int currentFileIndex = -1;
|
||||||
|
for (int i = 0; i < entryList.count(); i++) {
|
||||||
|
const QString & oneEntry = entryList.at(i);
|
||||||
|
urlList.append(QUrl::fromLocalFile(dir.absoluteFilePath(oneEntry)));
|
||||||
|
if (oneEntry == currentFileName) {
|
||||||
|
currentFileIndex = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -162,6 +193,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -172,6 +204,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,11 +227,10 @@ void MainWindow::dropEvent(QDropEvent *e)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QModelIndex modelIndex = m_playlistManager->loadPlaylist(urls);
|
// TODO: file/format filter?
|
||||||
if (modelIndex.isValid()) {
|
|
||||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
|
createPlaylist(urls);
|
||||||
play();
|
m_mediaPlayer->play();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadFile()
|
void MainWindow::loadFile()
|
||||||
|
@ -212,33 +244,39 @@ void MainWindow::loadFile()
|
||||||
urlList.append(QUrl::fromLocalFile(fileName));
|
urlList.append(QUrl::fromLocalFile(fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_playlistManager->loadPlaylist(urlList);
|
createPlaylist(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)
|
||||||
{
|
{
|
||||||
QUrl fileUrl(m_mediaPlayer->source());
|
QMediaPlaylist* playlist = m_playlistModel->playlist();
|
||||||
|
playlist->clear();
|
||||||
|
playlist->addMedia(urlList);
|
||||||
|
|
||||||
m_mediaPlayer->play();
|
connect(playlist, &QMediaPlaylist::playbackModeChanged, this, [=](QMediaPlaylist::PlaybackMode mode) {
|
||||||
|
switch (mode) {
|
||||||
ui->titleLabel->setText(fileUrl.fileName());
|
case QMediaPlaylist::CurrentItemInLoop:
|
||||||
ui->titleLabel->setToolTip(fileUrl.fileName());
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png"));
|
||||||
|
break;
|
||||||
if (fileUrl.isLocalFile()) {
|
case QMediaPlaylist::Loop:
|
||||||
QString filePath(fileUrl.toLocalFile());
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png"));
|
||||||
QString suffix(filePath.mid(filePath.lastIndexOf('.') + 1));
|
break;
|
||||||
suffix = suffix.toUpper();
|
case QMediaPlaylist::Sequential:
|
||||||
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-normal.png"));
|
||||||
#ifndef NO_TAGLIB
|
break;
|
||||||
TagLib::FileRef fileRef(filePath.toLocal8Bit().data());
|
// case QMediaPlaylist::Random:
|
||||||
|
// ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png"));
|
||||||
if (!fileRef.isNull() && fileRef.audioProperties()) {
|
// break;
|
||||||
TagLib::AudioProperties *prop = fileRef.audioProperties();
|
default:
|
||||||
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
|
break;
|
||||||
}
|
|
||||||
#endif // NO_TAGLIB
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
|
||||||
|
playlist->setCurrentIndex(index < 0 ? 0 : index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::centerWindow()
|
void MainWindow::centerWindow()
|
||||||
|
@ -262,7 +300,7 @@ void MainWindow::on_playBtn_clicked()
|
||||||
{
|
{
|
||||||
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
|
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
|
||||||
loadFile();
|
loadFile();
|
||||||
play();
|
m_mediaPlayer->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 {
|
||||||
|
@ -321,18 +359,27 @@ void MainWindow::on_playbackSlider_valueChanged(int value)
|
||||||
|
|
||||||
void MainWindow::on_prevBtn_clicked()
|
void MainWindow::on_prevBtn_clicked()
|
||||||
{
|
{
|
||||||
QModelIndex index(m_playlistManager->previousIndex());
|
// QMediaPlaylist::previous() won't work when in CurrentItemInLoop playmode,
|
||||||
m_playlistManager->setCurrentIndex(index);
|
// and also works not as intended when in other playmode, so do it manually...
|
||||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
QMediaPlaylist * playlist = m_playlistModel->playlist();
|
||||||
play();
|
if (playlist) {
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
QModelIndex index(m_playlistManager->nextIndex());
|
// see also: MainWindow::on_prevBtn_clicked()
|
||||||
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 == (count - 1) ? 0 : index + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_volumeBtn_clicked()
|
void MainWindow::on_volumeBtn_clicked()
|
||||||
|
@ -357,26 +404,71 @@ 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_mediaPlayer, &QMediaPlayer::metaDataChanged, this, [=](){
|
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentIndexChanged, this, [=](int currentItem) {
|
||||||
QMediaMetaData metadata(m_mediaPlayer->metaData());
|
bool isPlaying = m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState;
|
||||||
setAudioMetadataForDisplay(metadata.stringValue(QMediaMetaData::Title),
|
m_mediaPlayer->setSource(m_playlistModel->playlist()->currentMedia());
|
||||||
metadata.stringValue(QMediaMetaData::Author),
|
if (isPlaying) m_mediaPlayer->play();
|
||||||
metadata.stringValue(QMediaMetaData::AlbumTitle));
|
});
|
||||||
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentMediaChanged, this, [=](const QUrl &fileUrl) {
|
||||||
if (!coverArt.isNull()) {
|
ui->titleLabel->setText(fileUrl.fileName());
|
||||||
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
|
ui->titleLabel->setToolTip(fileUrl.fileName());
|
||||||
} else {
|
|
||||||
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
if (fileUrl.isLocalFile()) {
|
||||||
|
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) {
|
||||||
|
@ -414,71 +506,35 @@ void MainWindow::initConnections()
|
||||||
ui->volumeSlider->setValue(vol * 100);
|
ui->volumeSlider->setValue(vol * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, [=](QMediaPlayer::MediaStatus status){
|
// connect(m_mediaPlayer, static_cast<void(QMediaPlayer::*)(QMediaPlayer::Error)>(&QMediaPlayer::error),
|
||||||
if (status == QMediaPlayer::EndOfMedia) {
|
// this, [=](QMediaPlayer::Error error) {
|
||||||
switch (m_playbackMode) {
|
// switch (error) {
|
||||||
case MainWindow::CurrentItemOnce:
|
// default:
|
||||||
// do nothing
|
// break;
|
||||||
break;
|
// }
|
||||||
case MainWindow::CurrentItemInLoop:
|
// qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data());
|
||||||
// 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()
|
||||||
{
|
{
|
||||||
switch (m_playbackMode) {
|
QMediaPlaylist * playlist = m_playlistModel->playlist();
|
||||||
case MainWindow::CurrentItemOnce:
|
if (!playlist) return;
|
||||||
setProperty("playbackMode", MainWindow::CurrentItemInLoop);
|
|
||||||
|
switch (playlist->playbackMode()) {
|
||||||
|
case QMediaPlaylist::CurrentItemInLoop:
|
||||||
|
playlist->setPlaybackMode(QMediaPlaylist::Loop);
|
||||||
break;
|
break;
|
||||||
case MainWindow::CurrentItemInLoop:
|
case QMediaPlaylist::Loop:
|
||||||
setProperty("playbackMode", MainWindow::Sequential);
|
playlist->setPlaybackMode(QMediaPlaylist::Sequential);
|
||||||
break;
|
break;
|
||||||
case MainWindow::Sequential:
|
case QMediaPlaylist::Sequential:
|
||||||
setProperty("playbackMode", MainWindow::CurrentItemOnce);
|
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
25
mainwindow.h
25
mainwindow.h
|
@ -12,25 +12,17 @@ class QAudioOutput;
|
||||||
class QPropertyAnimation;
|
class QPropertyAnimation;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class PlaylistManager;
|
class PlaylistModel;
|
||||||
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);
|
||||||
|
|
||||||
|
@ -47,9 +39,8 @@ 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();
|
||||||
|
@ -63,25 +54,17 @@ 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;
|
||||||
PlaylistManager *m_playlistManager;
|
PlaylistModel *m_playlistModel = nullptr; // TODO: move playback logic to player.cpp
|
||||||
|
|
||||||
void initUiAndAnimation();
|
void initUiAndAnimation();
|
||||||
void initConnections();
|
void initConnections();
|
||||||
|
|
|
@ -209,7 +209,7 @@ QLabel#coverLabel {
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::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::SizeConstraint::SetDefaultConstraint</enum>
|
<enum>QLayout::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::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::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::Orientation::Horizontal</enum>
|
<enum>Qt::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::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -576,9 +576,9 @@ QLabel#coverLabel {
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QStackedWidget" name="pluginStackedWidget">
|
<widget class="QWidget" name="pluginWidget" native="true">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
|
@ -589,28 +589,6 @@ 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>
|
||||||
|
|
|
@ -1,255 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
// 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;
|
|
||||||
};
|
|
102
playlistmodel.cpp
Normal file
102
playlistmodel.cpp
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// 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));
|
||||||
|
}
|
52
playlistmodel.h
Normal file
52
playlistmodel.h
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// 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
|
653
qt/qmediaplaylist.cpp
Normal file
653
qt/qmediaplaylist.cpp
Normal file
|
@ -0,0 +1,653 @@
|
||||||
|
// 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"
|
96
qt/qmediaplaylist.h
Normal file
96
qt/qmediaplaylist.h
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// 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
|
112
qt/qmediaplaylist_p.h
Normal file
112
qt/qmediaplaylist_p.h
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// 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
|
605
qt/qplaylistfileparser.cpp
Normal file
605
qt/qplaylistfileparser.cpp
Normal file
|
@ -0,0 +1,605 @@
|
||||||
|
// 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
|
80
qt/qplaylistfileparser_p.h
Normal file
80
qt/qplaylistfileparser_p.h
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// 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
|
|
@ -15,7 +15,6 @@
|
||||||
<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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user