Compare commits
45 Commits
8aa68ccebd
...
master
Author | SHA1 | Date | |
---|---|---|---|
af38103c0d | |||
094a83512e | |||
5f6c89673c | |||
010e7162fd | |||
097b32c70d | |||
042988ffbb | |||
91c6672f5c | |||
dc4b9fc047 | |||
236798a957 | |||
15ffe5a85e | |||
c672a597b3 | |||
8ada0d2138 | |||
f55563d483 | |||
b6a64a2495 | |||
f17b722600 | |||
9bbfefaea1 | |||
cdadaa874e | |||
c789d04a6c | |||
e2dd7c9307 | |||
af8e61c124 | |||
5d609cdace | |||
b2eb29ab5d | |||
a3bc60c977 | |||
cf90e2d70c | |||
64c75263bf | |||
5092f9bafc | |||
044bb34390 | |||
7192c8a1b7 | |||
c9fc9346b7 | |||
54c604ee9d | |||
a2b9f94015 | |||
4b57dda342 | |||
04ed6d435c | |||
618a350e0d | |||
b88ee1d0f1 | |||
2a92f4ea7f | |||
f4374a0768 | |||
b01dfe17fd | |||
25eed8066b | |||
8ac558ebc6 | |||
a910e85d97 | |||
d28108f2e5 | |||
413711d073 | |||
34d3989e9e | |||
86a5e4f722 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
74
.github/workflows/windows.yml
vendored
Normal file
74
.github/workflows/windows.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
name: Windows CI
|
||||||
|
|
||||||
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
msvc-cmake-build:
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- qt_ver: '6.9.1'
|
||||||
|
vs: '2022'
|
||||||
|
aqt_arch: 'win64_msvc2022_64'
|
||||||
|
msvc_arch: 'x64'
|
||||||
|
|
||||||
|
runs-on: windows-2022
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Qt
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
arch: ${{ matrix.aqt_arch }}
|
||||||
|
version: ${{ matrix.qt_ver }}
|
||||||
|
modules: 'qtmultimedia qt5compat'
|
||||||
|
- 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
|
||||||
|
echo ::group::===== ECM =====
|
||||||
|
git clone -q https://invent.kde.org/frameworks/extra-cmake-modules.git dependencies_src/extra-cmake-modules
|
||||||
|
cmake .\dependencies_src\extra-cmake-modules -Bbuild_dependencies/extra-cmake-modules -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DBUILD_TESTING=OFF || goto :error
|
||||||
|
cmake --build build_dependencies/extra-cmake-modules --config Release --target=install || goto :error
|
||||||
|
echo ::endgroup::
|
||||||
|
echo ::group::===== FFmpeg =====
|
||||||
|
curl -fsSL -o ffmpeg-shared.7z https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build-shared.7z
|
||||||
|
7z x ffmpeg-shared.7z -y -o"dependencies_src"
|
||||||
|
set FFMPEG_ROOT=%PWD%\dependencies_src\ffmpeg-7.1.1-full_build-shared
|
||||||
|
echo ::endgroup::
|
||||||
|
echo ::group::===== Gperf (required by KCodecs) =====
|
||||||
|
choco install gperf
|
||||||
|
echo ::endgroup::
|
||||||
|
echo ::group::===== KCodecs =====
|
||||||
|
git clone -q https://invent.kde.org/frameworks/kcodecs.git dependencies_src/kcodecs
|
||||||
|
cmake .\dependencies_src\kcodecs -Bbuild_dependencies/kcodecs -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DBUILD_TESTING=OFF || goto :error
|
||||||
|
cmake --build build_dependencies/kcodecs --config Release --target=install || goto :error
|
||||||
|
echo ::endgroup::
|
||||||
|
echo ::group::===== 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
|
||||||
|
echo ::endgroup::
|
||||||
|
:: ------ app ------
|
||||||
|
cmake -Bbuild . -DUSE_QTEXTCODEC=ON -DCMAKE_INSTALL_PREFIX='%PWD%\build\' || goto :error
|
||||||
|
cmake --build build --config Release -j || goto :error
|
||||||
|
cmake --build build --config Release --target=install || goto :error
|
||||||
|
:: ------ pkg ------
|
||||||
|
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --core5compat --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: "${{ matrix.aqt_arch }}-qt${{ matrix.qt_ver }}-cmake-package"
|
||||||
|
path: build/bin/*
|
86
.gitignore
vendored
86
.gitignore
vendored
@ -1,77 +1,13 @@
|
|||||||
# This file is used to ignore files which are generated
|
# Common build folder
|
||||||
# ----------------------------------------------------------------------------
|
[Bb]uild/
|
||||||
|
build-*/
|
||||||
|
|
||||||
*~
|
# IDE folder
|
||||||
*.autosave
|
.vscode/
|
||||||
*.a
|
.idea/
|
||||||
*.core
|
|
||||||
*.moc
|
# User config file
|
||||||
*.o
|
CMakeLists.txt.user*
|
||||||
*.obj
|
|
||||||
*.orig
|
# Why, macOS, why?
|
||||||
*.rej
|
|
||||||
*.so
|
|
||||||
*.so.*
|
|
||||||
*_pch.h.cpp
|
|
||||||
*_resource.rc
|
|
||||||
*.qm
|
|
||||||
.#*
|
|
||||||
*.*#
|
|
||||||
core
|
|
||||||
!core/
|
|
||||||
tags
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.directory
|
|
||||||
*.debug
|
|
||||||
Makefile*
|
|
||||||
*.prl
|
|
||||||
*.app
|
|
||||||
moc_*.cpp
|
|
||||||
ui_*.h
|
|
||||||
qrc_*.cpp
|
|
||||||
Thumbs.db
|
|
||||||
*.res
|
|
||||||
*.rc
|
|
||||||
/.qmake.cache
|
|
||||||
/.qmake.stash
|
|
||||||
|
|
||||||
# qtcreator generated files
|
|
||||||
*.pro.user*
|
|
||||||
|
|
||||||
# xemacs temporary files
|
|
||||||
*.flc
|
|
||||||
|
|
||||||
# Vim temporary files
|
|
||||||
.*.swp
|
|
||||||
|
|
||||||
# Visual Studio generated files
|
|
||||||
*.ib_pdb_index
|
|
||||||
*.idb
|
|
||||||
*.ilk
|
|
||||||
*.pdb
|
|
||||||
*.sln
|
|
||||||
*.suo
|
|
||||||
*.vcproj
|
|
||||||
*vcproj.*.*.user
|
|
||||||
*.ncb
|
|
||||||
*.sdf
|
|
||||||
*.opensdf
|
|
||||||
*.vcxproj
|
|
||||||
*vcxproj.*
|
|
||||||
|
|
||||||
# MinGW generated files
|
|
||||||
*.Debug
|
|
||||||
*.Release
|
|
||||||
|
|
||||||
# Python byte code
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Binaries
|
|
||||||
# --------
|
|
||||||
*.dll
|
|
||||||
*.exe
|
|
||||||
|
|
||||||
# User Config
|
|
||||||
# -----------
|
|
||||||
*.user
|
|
||||||
*.user.*
|
|
||||||
|
133
CMakeLists.txt
133
CMakeLists.txt
@ -1,8 +1,10 @@
|
|||||||
cmake_minimum_required(VERSION 3.12)
|
cmake_minimum_required(VERSION 3.12)
|
||||||
|
|
||||||
project(pineapple-music LANGUAGES CXX)
|
project(pineapple-music LANGUAGES CXX VERSION 0.3.0)
|
||||||
|
|
||||||
include (GNUInstallDirs)
|
include (GNUInstallDirs)
|
||||||
|
include (FeatureSummary)
|
||||||
|
include (FetchContent)
|
||||||
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
|
||||||
@ -13,33 +15,50 @@ set(CMAKE_AUTORCC ON)
|
|||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
find_package(Qt6 6.5.1 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
|
option(USE_QTEXTCODEC "Use QTextCodec instead of QStringConverter, in case Qt is not built with ICU" OFF)
|
||||||
find_package(PkgConfig)
|
|
||||||
|
|
||||||
if (PKG_CONFIG_FOUND)
|
find_package(Qt6 6.6 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
|
||||||
pkg_check_modules(TagLib taglib IMPORTED_TARGET)
|
find_package(FFmpeg COMPONENTS avutil avformat)
|
||||||
|
find_package(TagLib 2.0.0)
|
||||||
|
find_package(KF6Codecs 6.1.0)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
kissfft
|
||||||
|
GIT_REPOSITORY https://github.com/mborgerding/kissfft.git
|
||||||
|
GIT_TAG f5f2a3b2f2cd02bf80639adb12cbeed125bdf420
|
||||||
|
)
|
||||||
|
set(KISSFFT_PKGCONFIG OFF CACHE BOOL "dep(kissfft): pkgconfig support")
|
||||||
|
set(KISSFFT_STATIC ON CACHE BOOL "dep(kissfft): static linking")
|
||||||
|
set(KISSFFT_TEST OFF CACHE BOOL "dep(kissfft): enable testing")
|
||||||
|
set(KISSFFT_TOOLS OFF CACHE BOOL "dep(kissfft): build tools")
|
||||||
|
FetchContent_MakeAvailable(kissfft)
|
||||||
|
|
||||||
|
if (USE_QTEXTCODEC)
|
||||||
|
find_package(Qt6 6.6 COMPONENTS Core5Compat REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PMUSIC_CPP_FILES
|
set (PMUSIC_CPP_FILES
|
||||||
main.cpp
|
main.cpp
|
||||||
mainwindow.cpp
|
mainwindow.cpp
|
||||||
seekableslider.cpp
|
seekableslider.cpp
|
||||||
playlistmodel.cpp
|
playlistmanager.cpp
|
||||||
singleapplicationmanager.cpp
|
singleapplicationmanager.cpp
|
||||||
|
lrcbar.cpp
|
||||||
qt/qplaylistfileparser.cpp
|
lyricsmanager.cpp
|
||||||
qt/qmediaplaylist.cpp
|
fftspectrum.cpp
|
||||||
|
playbackprogressindicator.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_HEADER_FILES
|
set (PMUSIC_HEADER_FILES
|
||||||
mainwindow.h
|
mainwindow.h
|
||||||
seekableslider.h
|
seekableslider.h
|
||||||
playlistmodel.h
|
playlistmanager.h
|
||||||
singleapplicationmanager.h
|
singleapplicationmanager.h
|
||||||
|
lrcbar.h
|
||||||
qt/qplaylistfileparser_p.h
|
lyricsmanager.h
|
||||||
qt/qmediaplaylist.h
|
fftspectrum.h
|
||||||
qt/qmediaplaylist_p.h
|
playbackprogressindicator.h
|
||||||
|
taskbarmanager.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_UI_FILES
|
set (PMUSIC_UI_FILES
|
||||||
@ -52,44 +71,67 @@ set (EXE_NAME pmusic)
|
|||||||
file (GLOB PMUSIC_TS_FILES languages/*.ts)
|
file (GLOB PMUSIC_TS_FILES languages/*.ts)
|
||||||
set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES})
|
set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES})
|
||||||
|
|
||||||
qt_create_translation(PMUSIC_QM_FILES ${PMUSIC_CPP_FILES_FOR_I18N} ${PMUSIC_TS_FILES})
|
|
||||||
|
|
||||||
add_executable(${EXE_NAME}
|
add_executable(${EXE_NAME}
|
||||||
${PMUSIC_HEADER_FILES}
|
${PMUSIC_HEADER_FILES}
|
||||||
${PMUSIC_CPP_FILES}
|
${PMUSIC_CPP_FILES}
|
||||||
${PMUSIC_UI_FILES}
|
${PMUSIC_UI_FILES}
|
||||||
resources.qrc
|
resources.qrc
|
||||||
|
|
||||||
# 3rd party code
|
|
||||||
FlacPic.h
|
|
||||||
ID3v2Pic.h
|
|
||||||
|
|
||||||
${PMUSIC_QM_FILES}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (NOT TagLib_FOUND)
|
qt_add_translations(${EXE_NAME}
|
||||||
message (WARNING "TagLib not found!")
|
TS_FILES
|
||||||
target_compile_definitions(${EXE_NAME} PRIVATE
|
${PMUSIC_TS_FILES}
|
||||||
NO_TAGLIB=1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
||||||
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp)
|
||||||
else ()
|
else ()
|
||||||
target_link_libraries(${EXE_NAME} PRIVATE PkgConfig::TagLib)
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
|
if (NOT TagLib_FOUND)
|
||||||
|
target_compile_definitions(${EXE_NAME} PRIVATE NO_TAGLIB=1)
|
||||||
|
else ()
|
||||||
|
target_link_libraries(${EXE_NAME} PRIVATE TagLib::tag)
|
||||||
|
endif ()
|
||||||
|
|
||||||
# Extra build settings
|
if (TARGET KF6::Codecs)
|
||||||
if (WIN32)
|
target_compile_definitions(${EXE_NAME} PRIVATE HAVE_KCODECS=1)
|
||||||
set_property (
|
target_link_libraries (${EXE_NAME} PRIVATE KF6::Codecs)
|
||||||
TARGET ${EXE_NAME}
|
endif ()
|
||||||
PROPERTY WIN32_EXECUTABLE true
|
|
||||||
)
|
if (FFmpeg_FOUND)
|
||||||
|
target_compile_definitions(${EXE_NAME} PRIVATE HAVE_FFMPEG=1)
|
||||||
|
target_link_libraries (${EXE_NAME} PRIVATE FFmpeg::avutil FFmpeg::avformat)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network kissfft::kissfft)
|
||||||
|
|
||||||
|
if (USE_QTEXTCODEC)
|
||||||
|
target_compile_definitions(${EXE_NAME} PRIVATE USE_QTEXTCODEC=1)
|
||||||
|
target_link_libraries(${EXE_NAME} PRIVATE Qt6::Core5Compat)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Install settings
|
# Install settings
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
# FIXME: try to avoid install to a "bin" subfolder under windows...
|
set_target_properties(${EXE_NAME} PROPERTIES
|
||||||
# when fixed, don't forget to update the CI config file...
|
WIN32_EXECUTABLE TRUE
|
||||||
|
)
|
||||||
|
elseif (APPLE)
|
||||||
|
set_source_files_properties(assets/icons/app-icon.icns PROPERTIES
|
||||||
|
MACOSX_PACKAGE_LOCATION "Resources"
|
||||||
|
)
|
||||||
|
target_sources(${EXE_NAME} PUBLIC assets/icons/app-icon.icns)
|
||||||
|
# See https://cmake.org/cmake/help/v3.15/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html
|
||||||
|
set_target_properties(${EXE_NAME} PROPERTIES
|
||||||
|
MACOSX_BUNDLE TRUE
|
||||||
|
MACOSX_BUNDLE_BUNDLE_NAME "Pineapple Music"
|
||||||
|
MACOSX_BUNDLE_GUI_IDENTIFIER net.blumia.pineapple-music
|
||||||
|
MACOSX_BUNDLE_ICON_FILE app-icon.icns # contains the .icns file name, *without* the path.
|
||||||
|
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||||
|
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||||
|
)
|
||||||
elseif (UNIX)
|
elseif (UNIX)
|
||||||
if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
set(CMAKE_INSTALL_PREFIX /usr)
|
set(CMAKE_INSTALL_PREFIX /usr)
|
||||||
@ -99,17 +141,18 @@ elseif (UNIX)
|
|||||||
install(
|
install(
|
||||||
FILES icons/app-icon.svg
|
FILES icons/app-icon.svg
|
||||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps"
|
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps"
|
||||||
RENAME pineapple-music.svg
|
RENAME net.blumia.pineapple-music.svg
|
||||||
)
|
)
|
||||||
|
|
||||||
# install shortcut
|
# install shortcut
|
||||||
install(
|
install(
|
||||||
FILES pineapple-music.desktop
|
FILES dist/net.blumia.pineapple-music.desktop
|
||||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications"
|
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications"
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (INSTALL_TARGETS_DEFAULT_ARGS
|
set (INSTALL_TARGETS_DEFAULT_ARGS
|
||||||
|
BUNDLE DESTINATION .
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Devel
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Devel
|
||||||
@ -120,16 +163,4 @@ install (
|
|||||||
${INSTALL_TARGETS_DEFAULT_ARGS}
|
${INSTALL_TARGETS_DEFAULT_ARGS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (WIN32)
|
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
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
263
FlacPic.h
@ -1,263 +0,0 @@
|
|||||||
/*
|
|
||||||
FLAC标签图片提取库 Ver 1.0
|
|
||||||
从FLAC文件中稳定、快捷、高效、便捷地提取出图片数据
|
|
||||||
支持BMP、JPEG、PNG、GIF图片格式
|
|
||||||
可将图片数据提取到文件或内存中,并能安全地释放内存
|
|
||||||
使用方式与ID3v2版本相同
|
|
||||||
ShadowPower 于2014/8/1 夜间
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _ShadowPower_FLACPIC___
|
|
||||||
#define _ShadowPower_FLACPIC___
|
|
||||||
#define _CRT_SECURE_NO_WARNINGS
|
|
||||||
#ifndef NULL
|
|
||||||
#define NULL 0
|
|
||||||
#endif
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <memory.h>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
typedef unsigned char byte;
|
|
||||||
|
|
||||||
namespace spFLAC {
|
|
||||||
//Flac元数据块头部结构体定义
|
|
||||||
struct FlacMetadataBlockHeader
|
|
||||||
{
|
|
||||||
byte flag; //标志位,高1位:是否为最后一个数据块,低7位:数据块类型
|
|
||||||
byte length[3]; //数据块长度,不含数据块头部
|
|
||||||
};
|
|
||||||
|
|
||||||
byte *pPicData = 0; //指向图片数据的指针
|
|
||||||
int picLength = 0; //存放图片数据长度
|
|
||||||
char picFormat[4] = {}; //存放图片数据的格式(扩展名)
|
|
||||||
|
|
||||||
//检测图片格式,参数1:数据,返回值:是否成功(不是图片则失败)
|
|
||||||
bool verificationPictureFormat(char *data)
|
|
||||||
{
|
|
||||||
//支持格式:JPEG/PNG/BMP/GIF
|
|
||||||
byte jpeg[2] = { 0xff, 0xd8 };
|
|
||||||
byte png[8] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
|
|
||||||
byte gif[6] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
|
|
||||||
byte gif2[6] = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 };
|
|
||||||
byte bmp[2] = { 0x42, 0x4d };
|
|
||||||
memset(&picFormat, 0, 4);
|
|
||||||
if (memcmp(data, &jpeg, 2) == 0)
|
|
||||||
{
|
|
||||||
strcpy(picFormat, "jpg");
|
|
||||||
}
|
|
||||||
else if (memcmp(data, &png, 8) == 0)
|
|
||||||
{
|
|
||||||
strcpy(picFormat, "png");
|
|
||||||
}
|
|
||||||
else if (memcmp(data, &gif, 6) == 0 || memcmp(data, &gif2, 6) == 0)
|
|
||||||
{
|
|
||||||
strcpy(picFormat, "gif");
|
|
||||||
}
|
|
||||||
else if (memcmp(data, &bmp, 2) == 0)
|
|
||||||
{
|
|
||||||
strcpy(picFormat, "bmp");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//安全释放内存
|
|
||||||
void freePictureData()
|
|
||||||
{
|
|
||||||
if (pPicData)
|
|
||||||
{
|
|
||||||
delete pPicData;
|
|
||||||
}
|
|
||||||
pPicData = 0;
|
|
||||||
picLength = 0;
|
|
||||||
memset(&picFormat, 0, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
//将图片提取到内存,参数1:文件路径,成功返回true
|
|
||||||
bool loadPictureData(const char *inFilePath)
|
|
||||||
{
|
|
||||||
freePictureData();
|
|
||||||
FILE *fp = NULL;
|
|
||||||
fp = fopen(inFilePath, "rb");
|
|
||||||
if (!fp) //如果打开失败
|
|
||||||
{
|
|
||||||
fp = NULL;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fseek(fp, 0, SEEK_SET); //设文件流指针到文件头部
|
|
||||||
byte magic[4] = {}; //存放校验数据
|
|
||||||
memset(&magic, 0, 4);
|
|
||||||
fread(&magic, 4, 1, fp); //读入校验数据
|
|
||||||
byte fLaC[4] = { 0x66, 0x4c, 0x61, 0x43 };
|
|
||||||
if (memcmp(&magic, &fLaC, 4) == 0)
|
|
||||||
{
|
|
||||||
//数据校验正确,文件类型为Flac
|
|
||||||
FlacMetadataBlockHeader fmbh; //创建Flac元数据块头部结构体
|
|
||||||
memset(&fmbh, 0, 4); //清空内存
|
|
||||||
fread(&fmbh, 4, 1, fp); //读入头部数据
|
|
||||||
//计算数据块长度,不含头部
|
|
||||||
int blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];
|
|
||||||
int loopCount = 0; //循环计数,防死
|
|
||||||
while ((fmbh.flag & 0x7f) != 6)
|
|
||||||
{
|
|
||||||
//如果数据类型不是图片,此处循环执行
|
|
||||||
loopCount++;
|
|
||||||
if (loopCount > 40)
|
|
||||||
{
|
|
||||||
//循环40次没有遇到末尾就直接停止
|
|
||||||
fclose(fp);
|
|
||||||
fp = NULL;
|
|
||||||
return false; //可能文件不正常
|
|
||||||
}
|
|
||||||
fseek(fp, blockLength, SEEK_CUR); //跳过数据块
|
|
||||||
if ((fmbh.flag & 0x80) == 0x80)
|
|
||||||
{
|
|
||||||
//已经是最后一个数据块了,仍然不是图片
|
|
||||||
fclose(fp);
|
|
||||||
fp = NULL;
|
|
||||||
return false; //没有找到图片数据
|
|
||||||
}
|
|
||||||
//取得下一数据块头部
|
|
||||||
memset(&fmbh, 0, 4); //清空内存
|
|
||||||
fread(&fmbh, 4, 1, fp); //读入头部数据
|
|
||||||
blockLength = fmbh.length[0] * 0x10000 + fmbh.length[1] * 0x100 + fmbh.length[2];//计算数据块长度
|
|
||||||
}
|
|
||||||
//此时已到图片数据块
|
|
||||||
|
|
||||||
int nonPicDataLength = 0; //非图片数据长度
|
|
||||||
fseek(fp, 4, SEEK_CUR); //信仰之跃
|
|
||||||
nonPicDataLength += 4;
|
|
||||||
char nextJumpLength[4]; //下次要跳的长度
|
|
||||||
fread(&nextJumpLength, 4, 1, fp); //读取安全跳跃距离
|
|
||||||
nonPicDataLength += 4;
|
|
||||||
int jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];//计算数据块长度
|
|
||||||
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump!!
|
|
||||||
nonPicDataLength += jumpLength;
|
|
||||||
fread(&nextJumpLength, 4, 1, fp);
|
|
||||||
nonPicDataLength += 4;
|
|
||||||
jumpLength = nextJumpLength[0] * 0x1000000 + nextJumpLength[1] * 0x10000 + nextJumpLength[2] * 0x100 + nextJumpLength[3];
|
|
||||||
fseek(fp, jumpLength, SEEK_CUR); //Let's Jump too!!
|
|
||||||
nonPicDataLength += jumpLength;
|
|
||||||
fseek(fp, 20, SEEK_CUR); //信仰之跃
|
|
||||||
nonPicDataLength += 20;
|
|
||||||
|
|
||||||
//非主流情况检测+获得文件格式
|
|
||||||
char tempData[20] = {};
|
|
||||||
memset(tempData, 0, 20);
|
|
||||||
fread(&tempData, 8, 1, fp);
|
|
||||||
fseek(fp, -8, SEEK_CUR); //回到原位
|
|
||||||
//判断40次,一位一位跳到文件头
|
|
||||||
bool ok = false; //是否正确识别出文件头
|
|
||||||
for (int i = 0; i < 40; i++)
|
|
||||||
{
|
|
||||||
//校验文件头
|
|
||||||
if (verificationPictureFormat(tempData))
|
|
||||||
{
|
|
||||||
ok = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//如果校验失败尝试继续向后校验
|
|
||||||
fseek(fp, 1, SEEK_CUR);
|
|
||||||
nonPicDataLength++;
|
|
||||||
fread(&tempData, 8, 1, fp);
|
|
||||||
fseek(fp, -8, SEEK_CUR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
fclose(fp);
|
|
||||||
fp = NULL;
|
|
||||||
freePictureData();
|
|
||||||
return false; //无法识别的数据
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----抵达图片数据区-----
|
|
||||||
picLength = blockLength - nonPicDataLength; //计算图片数据长度
|
|
||||||
pPicData = new byte[picLength]; //动态分配图片数据内存空间
|
|
||||||
memset(pPicData, 0, picLength); //清空图片数据内存
|
|
||||||
fread(pPicData, picLength, 1, fp); //得到图片数据
|
|
||||||
//------------------------
|
|
||||||
fclose(fp); //操作已完成,关闭文件。
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//校验失败,不是Flac
|
|
||||||
fclose(fp);
|
|
||||||
fp = NULL;
|
|
||||||
freePictureData();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//取得图片数据的长度
|
|
||||||
int getPictureLength()
|
|
||||||
{
|
|
||||||
return picLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
//取得指向图片数据的指针
|
|
||||||
byte *getPictureDataPtr()
|
|
||||||
{
|
|
||||||
return pPicData;
|
|
||||||
}
|
|
||||||
|
|
||||||
//取得图片数据的扩展名(指针)
|
|
||||||
char *getPictureFormat()
|
|
||||||
{
|
|
||||||
return picFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool writePictureDataToFile(const char *outFilePath)
|
|
||||||
{
|
|
||||||
FILE *fp = NULL;
|
|
||||||
if (picLength > 0)
|
|
||||||
{
|
|
||||||
fp = fopen(outFilePath, "wb"); //打开目标文件
|
|
||||||
if (fp) //打开成功
|
|
||||||
{
|
|
||||||
fwrite(pPicData, picLength, 1, fp); //写入文件
|
|
||||||
fclose(fp); //关闭
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false; //文件打开失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false; //没有图像数据
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//提取图片文件,参数1:输入文件,参数2:输出文件,返回值:是否成功
|
|
||||||
bool extractPicture(const char *inFilePath, const char *outFilePath)
|
|
||||||
{
|
|
||||||
if (loadPictureData(inFilePath)) //如果取得图片数据成功
|
|
||||||
{
|
|
||||||
if (writePictureDataToFile(outFilePath))
|
|
||||||
{
|
|
||||||
return true; //文件写出成功
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false; //文件写出失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false; //无图片数据
|
|
||||||
}
|
|
||||||
freePictureData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
441
ID3v2Pic.h
441
ID3v2Pic.h
@ -1,441 +0,0 @@
|
|||||||
/*
|
|
||||||
ID3v2标签图片提取库 Ver 1.0
|
|
||||||
支持ID3v2所有版本
|
|
||||||
从ID3v2标签中稳定、快捷、高效、便捷地提取出图片数据
|
|
||||||
支持BMP、JPEG、PNG、GIF图片格式
|
|
||||||
可将图片数据提取到文件或内存中,并能安全地释放内存
|
|
||||||
ShadowPower 于2014/8/1 上午
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _ShadowPower_ID3V2PIC___
|
|
||||||
#define _ShadowPower_ID3V2PIC___
|
|
||||||
#define _CRT_SECURE_NO_WARNINGS
|
|
||||||
#ifndef NULL
|
|
||||||
#define NULL 0
|
|
||||||
#endif
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <memory.h>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
typedef unsigned char byte;
|
|
||||||
|
|
||||||
namespace spID3 {
|
|
||||||
//ID3v2标签头部结构体定义
|
|
||||||
struct ID3V2Header
|
|
||||||
{
|
|
||||||
char identi[3];//ID3头部校验,必须为“ID3”否则认为不存在ID3标签
|
|
||||||
byte major; //ID3版本号,3是ID3v2.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
|
|
70
README.md
70
README.md
@ -1,49 +1,61 @@
|
|||||||
_**This is a not ready to use, toy project**_
|
## Read Before Use
|
||||||
|
|
||||||
Since **I** just need a simple player which *just works* right now, so I did many things in a dirty way. Don't feel so weird if you saw something I did in this project is using a bad approach.
|
Since **I** just need a simple player which *just works* right now, so I did many things in a dirty way. Don't feel so weird if you saw something I did in this project is using a bad approach.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
We have the following features:
|
||||||
|
|
||||||
|
- [Sidecar](https://en.wikipedia.org/wiki/Sidecar_file) lyrics file (`.lrc`) support with an optional desktop lyrics bar widget
|
||||||
|
- Chapter file support
|
||||||
|
- Sidecar chapter file support:
|
||||||
|
- [YouTube-style chapter](https://support.google.com/youtube/answer/9884579) saved to a plain text file with `.chp` suffix
|
||||||
|
- PotPlayer `.pbf` file, `[Bookmark]`s as chapters
|
||||||
|
- Built-in chapter support (require build with FFmpeg)
|
||||||
|
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
|
||||||
|
|
||||||
|
These features are not available, some of them are TBD and others are not planned:
|
||||||
|
|
||||||
|
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
||||||
|
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
||||||
|
- No music library management support and there won't be one!
|
||||||
|
- It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
|
||||||
|
- Limited system integration:
|
||||||
|
- No [SMTC](https://learn.microsoft.com/en-us/uwp/api/windows.media.systemmediatransportcontrols) support under Windows for now
|
||||||
|
- No [MPRIS](https://www.freedesktop.org/wiki/Specifications/mpris-spec/) support under Linux desktop for now
|
||||||
|
- Limited lyrics (`.lrc`) loading support:
|
||||||
|
- Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
|
||||||
|
- Multi-line lyrics and duplicated timestamps are not supported
|
||||||
|
- Extensions (Walaoke and A2 extension) are not supported
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Current state, we need:
|
Current state, we need:
|
||||||
|
|
||||||
- `cmake` as the build system.
|
- `cmake` as the build system.
|
||||||
- `qt6` with `qt6-multimedia` since we use it for playback.
|
- `qt6` with `qt6-multimedia` since we use it for playback.
|
||||||
- `taglib` to get the audio file properties.
|
- `kissfft` for FFT support (will be downloaded at configure-time by `cmake`).
|
||||||
- `pkg-config` to find the installed taglib.
|
- (optional) `taglib` to get the audio file properties.
|
||||||
|
- (optional) `kf6codecs` to get the lyrics encoding correct.
|
||||||
|
- (optional) `ffmpeg` to get the audio chapter properties.
|
||||||
|
- It's suggested to use the same FFmpeg version that Qt Multimedia uses, so we can reuse the FFmpeg library binaries that Qt uses.
|
||||||
|
- You can know the version by checking [Qt documentation's FFmpeg attribution page](https://doc.qt.io/qt-6.9/qtmultimedia-attribution-ffmpeg.html)
|
||||||
|
|
||||||
Then we can build it with any proper c++ compiler like g++ or msvc.
|
Then we can build it with any proper c++ compiler like g++ or msvc.
|
||||||
|
|
||||||
### Linux
|
Building it just requires normal cmake building steps:
|
||||||
|
|
||||||
Just normal build process as other program. Nothing special ;)
|
```shell
|
||||||
|
$ cmake -Bbuild
|
||||||
### Windows
|
$ cmake --build build
|
||||||
|
```
|
||||||
Install the depts manually is a nightmare. I use [KDE Craft](https://community.kde.org/Craft) but MSYS2 should also works. FYI currently this project is not intended to works under Windows (it should works and I also did some simple test though).
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
I don't have a mac, so no support at all.
|
|
||||||
|
|
||||||
## Help Translation!
|
## Help Translation!
|
||||||
|
|
||||||
[Translate this project on Transifex!](https://www.transifex.com/blumia/pineapple-music/)
|
[Translate this project on Codeberg's Weblate!](https://translate.codeberg.org/projects/pineapple-apps/pineapple-music/)
|
||||||
|
|
||||||
Feel free to open up an issue to request an new language to translate.
|
|
||||||
|
|
||||||
## About License
|
## About License
|
||||||
|
|
||||||
Since this is a toy repo, I don't spend much time about the license stuff. Currently this project use some assets and code from [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer), which have a very interesting license -- do whatever you want but cannot be used as homework -- obviously it's not a so called *free* license. I *may* do some license housecleaning works by replaceing the assets and code implementation when the code become reasonable, and the final codebase may probably released under MIT license.
|
Pineapple Music as a whole is licensed under MIT license. Individual files may have a different, but compatible license.
|
||||||
|
|
||||||
Anyway here is a list of file which is in non-free state (with license: do whatever you want but cannot be used as homework):
|
All *png* images inside `icons` folder are originally created by [@ShadowPower](https://github.com/ShadowPower/) for [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer). These images are licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) license, grant by the original author.
|
||||||
|
|
||||||
- All png images inside `icons` folder.
|
|
||||||
- seekableslider.{h,cpp}
|
|
||||||
|
|
||||||
And something from ShadowPlayer but in other license:
|
|
||||||
|
|
||||||
- {Flac,ID3v2}Pic.h : [AlbumCoverExtractor](https://github.com/ShadowPower/AlbumCoverExtractor), with [MIT License](https://github.com/ShadowPower/AlbumCoverExtractor/blob/master/LICENSE)
|
|
||||||
|
|
||||||
Also there are some source code which I copy-paste from Qt codebase, which released under BSD-3-Clause license by the Qt Company:
|
|
||||||
|
|
||||||
- playlistmodel.{h,cpp}
|
|
||||||
|
31
appveyor.yml
31
appveyor.yml
@ -3,11 +3,10 @@ image:
|
|||||||
environment:
|
environment:
|
||||||
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
||||||
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
||||||
PKG_CONFIG_PATH: C:\projects\pir\lib\pkgconfig
|
|
||||||
matrix:
|
matrix:
|
||||||
- build_name: mingw1120_64_qt6_5
|
- build_name: mingw1120_64_qt6_7
|
||||||
QTPATH: C:\Qt\6.5\mingw_64
|
QTPATH: C:\Qt\6.8\mingw_64
|
||||||
MINGW64: C:\Qt\Tools\mingw1120_64
|
MINGW64: C:\Qt\Tools\mingw1310_64
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- mkdir %CMAKE_INSTALL_ROOT%
|
- mkdir %CMAKE_INSTALL_ROOT%
|
||||||
@ -21,21 +20,33 @@ install:
|
|||||||
build_script:
|
build_script:
|
||||||
# prepare
|
# prepare
|
||||||
- mkdir 3rdparty
|
- mkdir 3rdparty
|
||||||
- cinst ninja
|
- choco install ninja
|
||||||
- cinst pkgconfiglite
|
- choco install gperf
|
||||||
# build taglib
|
|
||||||
- cd 3rdparty
|
- cd 3rdparty
|
||||||
- git clone -q https://github.com/taglib/taglib.git
|
# install ECM
|
||||||
|
- git clone -q https://invent.kde.org/frameworks/extra-cmake-modules.git
|
||||||
|
- git rev-parse HEAD
|
||||||
|
- cd extra-cmake-modules
|
||||||
|
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_TESTING=OFF
|
||||||
|
- cmake --build . --target install
|
||||||
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
|
# build kcodecs
|
||||||
|
- git clone -q https://invent.kde.org/frameworks/kcodecs.git
|
||||||
|
- cd kcodecs
|
||||||
|
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_TESTING=OFF
|
||||||
|
- cmake --build . --target install
|
||||||
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
|
# build taglib
|
||||||
|
- git clone --recurse-submodules -q https://github.com/taglib/taglib.git
|
||||||
- cd taglib
|
- cd taglib
|
||||||
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON
|
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON
|
||||||
- cmake --build .
|
|
||||||
- cmake --build . --target install
|
- cmake --build . --target install
|
||||||
- cd %APPVEYOR_BUILD_FOLDER%
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
- tree %PACKAGE_INSTALL_ROOT% /f
|
- tree %PACKAGE_INSTALL_ROOT% /f
|
||||||
# finally...
|
# finally...
|
||||||
- mkdir build
|
- mkdir build
|
||||||
- cd build
|
- cd build
|
||||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX='%cd%'
|
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%PACKAGE_INSTALL_ROOT% -DCMAKE_INSTALL_PREFIX='%cd%'
|
||||||
- cmake --build .
|
- cmake --build .
|
||||||
- cmake --build . --target install
|
- cmake --build . --target install
|
||||||
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...
|
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...
|
||||||
|
BIN
assets/icons/app-icon.icns
Normal file
BIN
assets/icons/app-icon.icns
Normal file
Binary file not shown.
BIN
assets/icons/app-icon.ico
Normal file
BIN
assets/icons/app-icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
17
assets/pineapple-music.rc
Normal file
17
assets/pineapple-music.rc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
IDI_ICON1 ICON DISCARDABLE "icons/app-icon.ico"
|
||||||
|
1 VERSIONINFO
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904E4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "FileDescription", "Pineapple Music - Music Player"
|
||||||
|
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2024 Gary Wang"
|
||||||
|
VALUE "ProductName", "Pineapple Music"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
@ -3,7 +3,7 @@ Categories=Audio;Player;
|
|||||||
Comment=Pineapple Music Audio Player.
|
Comment=Pineapple Music Audio Player.
|
||||||
Exec=pmusic %F
|
Exec=pmusic %F
|
||||||
GenericName=Music
|
GenericName=Music
|
||||||
Icon=pineapple-music
|
Icon=net.blumia.pineapple-music
|
||||||
Keywords=Picture;Image;Viewer;Jpg;Jpeg;Png;
|
Keywords=Picture;Image;Viewer;Jpg;Jpeg;Png;
|
||||||
MimeType=application/ogg;application/x-ogg;audio/ogg;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/ogg;video/x-ogm;video/x-ogm+ogg;video/x-theora+ogg;video/x-theora;audio/x-speex;audio/opus;application/x-flac;audio/flac;audio/x-flac;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-msvideo;audio/x-pn-windows-acm;video/divx;video/msvideo;video/vnd.divx;video/avi;video/x-avi;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/vnd.rn-realaudio;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-real-audio;audio/x-realaudio;video/vnd.rn-realvideo;audio/mpeg;audio/mpg;audio/mp1;audio/mp2;audio/mp3;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpg;video/mp2t;video/mpeg;video/mpeg-system;video/x-mpeg;video/x-mpeg2;video/x-mpeg-system;application/mpeg4-iod;application/mpeg4-muxcodetable;application/x-extension-m4a;application/x-extension-mp4;audio/aac;audio/m4a;audio/mp4;audio/x-m4a;audio/x-aac;video/mp4;video/mp4v-es;video/x-m4v;application/x-quicktime-media-link;application/x-quicktimeplayer;video/quicktime;application/x-matroska;audio/x-matroska;video/x-matroska;video/webm;audio/webm;audio/3gpp;audio/3gpp2;audio/AMR;audio/AMR-WB;video/3gp;video/3gpp;video/3gpp2;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtsp;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/icy;x-scheme-handler/icyx;application/x-cd-image;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player;application/ram;application/xspf+xml;audio/mpegurl;audio/x-mpegurl;audio/scpls;audio/x-scpls;text/google-video-pointer;text/x-google-video-pointer;video/vnd.mpegurl;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.ms-wpl;application/sdp;audio/dv;video/dv;audio/x-aiff;audio/x-pn-aiff;video/x-anim;video/x-nsv;video/fli;video/flv;video/x-flc;video/x-fli;video/x-flv;audio/wav;audio/x-pn-au;audio/x-pn-wav;audio/x-wav;audio/x-adpcm;audio/ac3;audio/eac3;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/basic;audio/midi;audio/x-ape;audio/x-gsm;audio/x-musepack;audio/x-tta;audio/x-wavpack;audio/x-shorten;application/x-shockwave-flash;application/x-flash-video;misc/ultravox;image/vnd.rn-realpix;audio/x-it;audio/x-mod;audio/x-s3m;audio/x-xm;application/mxf;
|
MimeType=application/ogg;application/x-ogg;audio/ogg;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/ogg;video/x-ogm;video/x-ogm+ogg;video/x-theora+ogg;video/x-theora;audio/x-speex;audio/opus;application/x-flac;audio/flac;audio/x-flac;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-msvideo;audio/x-pn-windows-acm;video/divx;video/msvideo;video/vnd.divx;video/avi;video/x-avi;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/vnd.rn-realaudio;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-real-audio;audio/x-realaudio;video/vnd.rn-realvideo;audio/mpeg;audio/mpg;audio/mp1;audio/mp2;audio/mp3;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpg;video/mp2t;video/mpeg;video/mpeg-system;video/x-mpeg;video/x-mpeg2;video/x-mpeg-system;application/mpeg4-iod;application/mpeg4-muxcodetable;application/x-extension-m4a;application/x-extension-mp4;audio/aac;audio/m4a;audio/mp4;audio/x-m4a;audio/x-aac;video/mp4;video/mp4v-es;video/x-m4v;application/x-quicktime-media-link;application/x-quicktimeplayer;video/quicktime;application/x-matroska;audio/x-matroska;video/x-matroska;video/webm;audio/webm;audio/3gpp;audio/3gpp2;audio/AMR;audio/AMR-WB;video/3gp;video/3gpp;video/3gpp2;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtsp;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/icy;x-scheme-handler/icyx;application/x-cd-image;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player;application/ram;application/xspf+xml;audio/mpegurl;audio/x-mpegurl;audio/scpls;audio/x-scpls;text/google-video-pointer;text/x-google-video-pointer;video/vnd.mpegurl;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.ms-wpl;application/sdp;audio/dv;video/dv;audio/x-aiff;audio/x-pn-aiff;video/x-anim;video/x-nsv;video/fli;video/flv;video/x-flc;video/x-fli;video/x-flv;audio/wav;audio/x-pn-au;audio/x-pn-wav;audio/x-wav;audio/x-adpcm;audio/ac3;audio/eac3;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/basic;audio/midi;audio/x-ape;audio/x-gsm;audio/x-musepack;audio/x-tta;audio/x-wavpack;audio/x-shorten;application/x-shockwave-flash;application/x-flash-video;misc/ultravox;image/vnd.rn-realpix;audio/x-it;audio/x-mod;audio/x-s3m;audio/x-xm;application/mxf;
|
||||||
Name=Pineapple Music
|
Name=Pineapple Music
|
159
fftspectrum.cpp
Normal file
159
fftspectrum.cpp
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "fftspectrum.h"
|
||||||
|
|
||||||
|
#include <QAudioBuffer>
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
#include <QAudioBufferOutput>
|
||||||
|
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
#include <kissfft.hh>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FFTSpectrum::FFTSpectrum(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
, m_audioBufferOutput(new QAudioBufferOutput(this))
|
||||||
|
#endif // #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
{
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
connect(this, &FFTSpectrum::mediaPlayerChanged, this, [=]() {
|
||||||
|
if (m_mediaPlayer) {
|
||||||
|
m_mediaPlayer->setAudioBufferOutput(m_audioBufferOutput);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_audioBufferOutput, &QAudioBufferOutput::audioBufferReceived, this, [=](const QAudioBuffer& buffer) {
|
||||||
|
if (!isVisible()) return;
|
||||||
|
const QAudioFormat& fmt = buffer.format();
|
||||||
|
const QAudioFormat::SampleFormat sampleFormat = fmt.sampleFormat();
|
||||||
|
QAudioFormat::ChannelConfig channelConfig = fmt.channelConfig();
|
||||||
|
const QFlags supportedChannelConfig({ QAudioFormat::ChannelConfigMono, QAudioFormat::ChannelConfigStereo });
|
||||||
|
const int frameCount = buffer.frameCount();
|
||||||
|
kissfft<float> fft(frameCount, false);
|
||||||
|
std::vector<kissfft<float>::cpx_t> samples(frameCount);
|
||||||
|
std::vector<kissfft<float>::cpx_t> mass(frameCount);
|
||||||
|
if (sampleFormat == QAudioFormat::Int16 && supportedChannelConfig.testFlag(channelConfig)) {
|
||||||
|
if (channelConfig == QAudioFormat::ChannelConfigMono) {
|
||||||
|
const QAudioBuffer::S16M* data = buffer.constData<QAudioBuffer::S16M>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontCenter) / float(32768));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const QAudioBuffer::S16S* data = buffer.constData<QAudioBuffer::S16S>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontLeft) / float(32768));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sampleFormat == QAudioFormat::Int32 && supportedChannelConfig.testFlag(channelConfig)) {
|
||||||
|
if (channelConfig == QAudioFormat::ChannelConfigMono) {
|
||||||
|
const QAudioBuffer::S32M* data = buffer.constData<QAudioBuffer::S32M>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontCenter) / float(2147483647));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const QAudioBuffer::S32S* data = buffer.constData<QAudioBuffer::S32S>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontLeft) / float(2147483647));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sampleFormat == QAudioFormat::Float && supportedChannelConfig.testFlag(channelConfig)) {
|
||||||
|
if (channelConfig == QAudioFormat::ChannelConfigMono) {
|
||||||
|
const QAudioBuffer::F32M* data = buffer.constData<QAudioBuffer::F32M>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontCenter));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const QAudioBuffer::F32S* data = buffer.constData<QAudioBuffer::F32S>();
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
samples[i].real(data[i].value(QAudioFormat::FrontLeft));
|
||||||
|
samples[i].imag(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "Unsupported format or channel config:" << sampleFormat << channelConfig;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Apply Hanning window to reduce spectral leakage
|
||||||
|
for (int i = 0; i < frameCount; ++i) {
|
||||||
|
float window = 0.5f * (1.0f - cos(2.0f * M_PI * i / (frameCount - 1)));
|
||||||
|
samples[i].real(samples[i].real() * window);
|
||||||
|
}
|
||||||
|
fft.transform(samples.data(), mass.data());
|
||||||
|
// Use only the first half of FFT result (positive frequencies)
|
||||||
|
int spectrumSize = frameCount / 2;
|
||||||
|
m_freq.resize(spectrumSize);
|
||||||
|
for (int i = 0; i < spectrumSize; i++) {
|
||||||
|
m_freq[i] = sqrt(mass[i].real() * mass[i].real() + mass[i].imag() * mass[i].imag());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove DC component and very low frequencies (like most spectrum analyzers do)
|
||||||
|
// DC component (0 Hz) is usually not musically relevant
|
||||||
|
m_freq[0] = 0;
|
||||||
|
|
||||||
|
// Optionally remove the first few bins (very low frequencies below ~20Hz)
|
||||||
|
// Calculate how many bins correspond to frequencies below 20Hz
|
||||||
|
int sampleRate = fmt.sampleRate();
|
||||||
|
int lowFreqCutoff = std::min(3, (20 * frameCount) / (sampleRate * 2)); // Limit to first 3 bins max
|
||||||
|
for (int i = 1; i <= lowFreqCutoff && i < spectrumSize; i++) {
|
||||||
|
m_freq[i] *= (float(i) / float(lowFreqCutoff + 1)); // Gradual fade-in instead of hard cut
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& val : m_freq) {
|
||||||
|
val = log10(val + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
|
resize(490, 420);
|
||||||
|
}
|
||||||
|
|
||||||
|
FFTSpectrum::~FFTSpectrum()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFTSpectrum::setMediaPlayer(QMediaPlayer* player)
|
||||||
|
{
|
||||||
|
m_mediaPlayer = player;
|
||||||
|
emit mediaPlayerChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFTSpectrum::paintEvent(QPaintEvent* e)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
|
||||||
|
if (!m_freq.empty()) {
|
||||||
|
int width = this->width();
|
||||||
|
int height = this->height();
|
||||||
|
int barWidth = std::max(1, width / static_cast<int>(m_freq.size()));
|
||||||
|
|
||||||
|
for (size_t i = 0; i < m_freq.size(); i++) {
|
||||||
|
// Use sqrt to compress the range similar to original, but safer
|
||||||
|
int barHeight = static_cast<int>(sqrt(std::max(0.0f, m_freq[i])) * height * 0.5);
|
||||||
|
barHeight = std::max(0, std::min(barHeight, height));
|
||||||
|
|
||||||
|
// Calculate alpha based on frequency value, similar to original
|
||||||
|
int alpha = std::min(255, static_cast<int>(140 * m_freq[i]) + 90);
|
||||||
|
alpha = std::max(90, alpha);
|
||||||
|
|
||||||
|
QColor color(70, 130, 180, alpha);
|
||||||
|
painter.fillRect(static_cast<int>(i) * barWidth, height - barHeight, barWidth, barHeight, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
31
fftspectrum.h
Normal file
31
fftspectrum.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
|
||||||
|
class QAudioBufferOutput;
|
||||||
|
class FFTSpectrum : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QMediaPlayer * mediaPlayer MEMBER m_mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
|
||||||
|
public:
|
||||||
|
explicit FFTSpectrum(QWidget* parent);
|
||||||
|
~FFTSpectrum();
|
||||||
|
|
||||||
|
void setMediaPlayer(QMediaPlayer* player);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void mediaPlayerChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMediaPlayer* m_mediaPlayer = nullptr;
|
||||||
|
QAudioBufferOutput* m_audioBufferOutput = nullptr;
|
||||||
|
std::vector<float> m_freq;
|
||||||
|
};
|
BIN
icons/media-repeat-single.png
Normal file
BIN
icons/media-repeat-single.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
icons/skin.png
Normal file
BIN
icons/skin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -1,112 +1,124 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE TS>
|
<!DOCTYPE TS>
|
||||||
<TS version="2.1">
|
<TS version="2.1">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="106"/>
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="108"/>
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="110"/>
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="238"/>
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="240"/>
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
<source>Pineapple Player</source>
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="231"/>
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
<source>^</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="297"/>
|
|
||||||
<source>No song loaded...</source>
|
<source>No song loaded...</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="304"/>
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
<source>Drag and drop file to load</source>
|
<source>Drag and drop file to load</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
<location filename="../mainwindow.ui" line="320"/>
|
<source>Lrc</source>
|
||||||
<source>0:00</source>
|
<comment>Lyrics</comment>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="116"/>
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="121"/>
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="126"/>
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>QMediaPlaylist</name>
|
<name>PlaybackProgressIndicator</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../qt/qmediaplaylist.cpp" line="460"/>
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
<source>The file could not be accessed.</source>
|
<source>Time</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../qt/qplaylistfileparser.cpp" line="301"/>
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
<source>%1 playlist type is unknown</source>
|
<source>Chapter Name</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>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>main</name>
|
<name>main</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../main.cpp" line="28"/>
|
<location filename="../main.cpp" line="27"/>
|
||||||
<source>File list.</source>
|
<source>File list.</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
@ -1,114 +1,126 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE TS>
|
<!DOCTYPE TS>
|
||||||
<TS version="2.1" language="zh_CN">
|
<TS version="2.1" language="zh_CN">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation>(间奏…)</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="106"/>
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation>单声道</translation>
|
<translation>单声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="108"/>
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation>立体声</translation>
|
<translation>立体声</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="110"/>
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation>%1 声道</translation>
|
<translation>%1 声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="238"/>
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation>选择要播放的曲目</translation>
|
<translation>选择要播放的曲目</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="240"/>
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation>音频文件</translation>
|
<translation>音频文件</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation>选择图片作为背景皮肤</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation>图片文件 (*.jpg *.jpeg *.png *.gif)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation>基于下列自由软件库:</translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
<source>Pineapple Player</source>
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
<translation type="unfinished"></translation>
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
|
<translation>菠萝音乐</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="231"/>
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
<source>^</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<location filename="../mainwindow.ui" line="297"/>
|
|
||||||
<source>No song loaded...</source>
|
<source>No song loaded...</source>
|
||||||
<translation>未加载曲目...</translation>
|
<translation>未加载曲目...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="304"/>
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
<source>Drag and drop file to load</source>
|
<source>Drag and drop file to load</source>
|
||||||
<translation>拖放文件来播放</translation>
|
<translation>拖放文件来播放</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
<location filename="../mainwindow.ui" line="320"/>
|
<source>Lrc</source>
|
||||||
<source>0:00</source>
|
<comment>Lyrics</comment>
|
||||||
<translation type="unfinished"></translation>
|
<translation>歌词</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="116"/>
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation>关于</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation>打开</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation>采样率: %1 Hz</translation>
|
<translation>采样率: %1 Hz</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="121"/>
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation>比特率: %1 Kbps</translation>
|
<translation>比特率: %1 Kbps</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="126"/>
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation>声道数: %1</translation>
|
<translation>声道数: %1</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>QMediaPlaylist</name>
|
<name>PlaybackProgressIndicator</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../qt/qmediaplaylist.cpp" line="460"/>
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
<source>The file could not be accessed.</source>
|
<source>Time</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation>时间</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../qt/qplaylistfileparser.cpp" line="301"/>
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
<source>%1 playlist type is unknown</source>
|
<source>Chapter Name</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation>章节名称</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>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>main</name>
|
<name>main</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../main.cpp" line="28"/>
|
<location filename="../main.cpp" line="27"/>
|
||||||
<source>File list.</source>
|
<source>File list.</source>
|
||||||
<translation>文件列表</translation>
|
<translation>文件列表。</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
</TS>
|
</TS>
|
||||||
|
128
lrcbar.cpp
Normal file
128
lrcbar.cpp
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "lrcbar.h"
|
||||||
|
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
LrcBar::LrcBar(QWidget *parent)
|
||||||
|
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
|
||||||
|
, m_lrcMgr(new LyricsManager(this))
|
||||||
|
{
|
||||||
|
m_font.setPointSize(30);
|
||||||
|
m_font.setStyleStrategy(QFont::PreferAntialias);
|
||||||
|
|
||||||
|
QSize windowSize(sizeHint());
|
||||||
|
|
||||||
|
QFontMetrics fm(m_font);
|
||||||
|
m_baseLinearGradient.setColorAt(0, QColor(20, 100, 200));
|
||||||
|
m_baseLinearGradient.setColorAt(1, QColor(0, 80, 255));
|
||||||
|
m_baseLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||||
|
m_baseLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||||
|
m_maskLinearGradient.setColorAt(0, QColor(255, 128, 0));
|
||||||
|
m_maskLinearGradient.setColorAt(0.5, QColor(255, 255, 0));
|
||||||
|
m_maskLinearGradient.setColorAt(1, QColor(255, 128, 0));
|
||||||
|
m_maskLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||||
|
m_maskLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||||
|
|
||||||
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
|
setMouseTracking(true);
|
||||||
|
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
|
||||||
|
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
|
||||||
|
windowSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
LrcBar::~LrcBar()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LrcBar::loadLyrics(QString filepath)
|
||||||
|
{
|
||||||
|
m_currentTimeMs = 0;
|
||||||
|
return m_lrcMgr->loadLyrics(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
|
||||||
|
{
|
||||||
|
if (!isVisible()) return;
|
||||||
|
|
||||||
|
m_currentTimeMs = timestampMs;
|
||||||
|
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize LrcBar::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(1000, 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::mouseMoveEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->buttons() & Qt::LeftButton) {
|
||||||
|
window()->windowHandle()->startSystemMove();
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QWidget::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
if (underMouse()) {
|
||||||
|
painter.setBrush(QBrush(QColor(255, 255, 255, 120)));
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
painter.drawRect(0, 0, width(), height());
|
||||||
|
}
|
||||||
|
painter.setFont(m_font);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
QString curLrc(m_lrcMgr->lyrics().trimmed());
|
||||||
|
if (curLrc.isEmpty()) {
|
||||||
|
curLrc = m_lrcMgr->hasLyrics() ? tr("(Interlude...)")
|
||||||
|
: QCoreApplication::translate("MainWindow", "Pineapple Music", nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFontMetrics fm(m_font);
|
||||||
|
int lrcWidth = fm.horizontalAdvance(curLrc);
|
||||||
|
int maskWidth = lrcWidth * m_lrcMgr->maskPercent(m_currentTimeMs);
|
||||||
|
int startOffsetX = 0;
|
||||||
|
|
||||||
|
if (fm.horizontalAdvance(curLrc) < width()) {
|
||||||
|
startOffsetX = (width() - lrcWidth) / 2;
|
||||||
|
} else {
|
||||||
|
if (maskWidth < width() / 2) {
|
||||||
|
startOffsetX = 0;
|
||||||
|
} else if (lrcWidth - maskWidth < width() / 2) {
|
||||||
|
startOffsetX = width() - lrcWidth;
|
||||||
|
} else {
|
||||||
|
startOffsetX = 0 - (maskWidth - width() / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shadow
|
||||||
|
painter.setPen(QColor(0, 0, 0, 80));
|
||||||
|
painter.drawText(startOffsetX + 2, 2, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
// text itself
|
||||||
|
painter.setPen(QPen(m_baseLinearGradient, 0));
|
||||||
|
painter.drawText(startOffsetX, 0, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
// mask
|
||||||
|
painter.setPen(QPen(m_maskLinearGradient, 0));
|
||||||
|
painter.drawText(startOffsetX, 0, maskWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::enterEvent(QEnterEvent *)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LrcBar::leaveEvent(QEvent *)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
36
lrcbar.h
Normal file
36
lrcbar.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLinearGradient>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class LyricsManager;
|
||||||
|
class LrcBar : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit LrcBar(QWidget *parent);
|
||||||
|
~LrcBar();
|
||||||
|
|
||||||
|
bool loadLyrics(QString filepath);
|
||||||
|
void playbackPositionChanged(int timestampMs, int totalTimeMs);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
void enterEvent(QEnterEvent *) override;
|
||||||
|
void leaveEvent(QEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_currentTimeMs = 0;
|
||||||
|
LyricsManager * m_lrcMgr;
|
||||||
|
QFont m_font;
|
||||||
|
QLinearGradient m_baseLinearGradient;
|
||||||
|
QLinearGradient m_maskLinearGradient;
|
||||||
|
bool m_underMouse = false;
|
||||||
|
};
|
213
lyricsmanager.cpp
Normal file
213
lyricsmanager.cpp
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStringConverter>
|
||||||
|
|
||||||
|
#ifdef HAVE_KCODECS
|
||||||
|
#include <KCharsets>
|
||||||
|
#include <KCodecs>
|
||||||
|
#include <KEncodingProber>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_QTEXTCODEC
|
||||||
|
#include <QTextCodec>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcLyrics, "pmusic.lyrics")
|
||||||
|
Q_LOGGING_CATEGORY(lcLyricsParser, "pmusic.lyrics.parser")
|
||||||
|
|
||||||
|
LyricsManager::LyricsManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricsManager::~LyricsManager()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LyricsManager::loadLyrics(QString filepath)
|
||||||
|
{
|
||||||
|
// reset state
|
||||||
|
reset();
|
||||||
|
|
||||||
|
// check and load file
|
||||||
|
QFileInfo fileInfo(filepath);
|
||||||
|
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
||||||
|
}
|
||||||
|
if (!fileInfo.exists()) return false;
|
||||||
|
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray fileContent(file.readAll());
|
||||||
|
QStringList lines;
|
||||||
|
#ifdef HAVE_KCODECS
|
||||||
|
KEncodingProber prober(KEncodingProber::Universal);
|
||||||
|
prober.feed(fileContent);
|
||||||
|
QByteArray encoding(prober.encoding());
|
||||||
|
qCDebug(lcLyrics) << "Detected encoding:" << QString(encoding) << "with confidence" << prober.confidence();
|
||||||
|
#ifdef USE_QTEXTCODEC
|
||||||
|
qCDebug(lcLyrics) << "QTextCodec is used instead of QStringConverter.";
|
||||||
|
QTextCodec *codec = QTextCodec::codecForName(encoding);
|
||||||
|
if (codec) {
|
||||||
|
lines = codec->toUnicode(fileContent).split('\n');
|
||||||
|
} else {
|
||||||
|
lines = QString(fileContent).split('\n');
|
||||||
|
qCDebug(lcLyrics) << "No codec for the detected encoding. Available codecs are:" << QTextCodec::availableCodecs();
|
||||||
|
qCDebug(lcLyrics) << "KCodecs offers these encodings:" << KCharsets::charsets()->availableEncodingNames();
|
||||||
|
}
|
||||||
|
#else // NOT USE_QTEXTCODEC
|
||||||
|
auto toUtf16 = QStringDecoder(encoding);
|
||||||
|
// Don't use `QStringConverter::availableCodecs().contains(QString(encoding))` here, since the charset
|
||||||
|
// encoding name might not match, e.g. GB18030 (from availableCodecs) != gb18030 (from KEncodingProber)
|
||||||
|
if (toUtf16.isValid()) {
|
||||||
|
QString decodedResult = toUtf16(fileContent);
|
||||||
|
lines = decodedResult.split('\n');
|
||||||
|
} else {
|
||||||
|
qCDebug(lcLyrics) << "No codec for the detected encoding. Available codecs are:" << QStringConverter::availableCodecs();
|
||||||
|
qCDebug(lcLyrics) << "KCodecs offers these encodings:" << KCharsets::charsets()->availableEncodingNames();
|
||||||
|
lines = QString(fileContent).split('\n');
|
||||||
|
}
|
||||||
|
#endif // USE_QTEXTCODEC
|
||||||
|
#else // NOT HAVE_KCODECS
|
||||||
|
lines = QString(fileContent).split('\n');
|
||||||
|
#endif // HAVE_KCODECS
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// parse lyrics timestamp
|
||||||
|
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
|
||||||
|
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2,3})\](.*))regex");
|
||||||
|
bool tagSectionPassed = false;
|
||||||
|
|
||||||
|
for (QString line : std::as_const(lines)) {
|
||||||
|
line = line.trimmed();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
|
||||||
|
if (!tagSectionPassed) {
|
||||||
|
QRegularExpressionMatch tagMatch = tagRegex.match(line);
|
||||||
|
if (tagMatch.hasMatch()) {
|
||||||
|
QString tag(tagMatch.captured(1));
|
||||||
|
if (tag == QLatin1String("offset")) {
|
||||||
|
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
|
||||||
|
m_timeOffset = -tagMatch.captured(2).toInt();
|
||||||
|
qCDebug(lcLyricsParser) << m_timeOffset;
|
||||||
|
}
|
||||||
|
qCDebug(lcLyricsParser) << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<int> timestamps;
|
||||||
|
QString currentLrc;
|
||||||
|
QRegularExpressionMatch match = lrcRegex.match(line);
|
||||||
|
while (match.hasMatch()) {
|
||||||
|
tagSectionPassed = true;
|
||||||
|
timestamps.append(parseTimeToMilliseconds(match.captured(1)));
|
||||||
|
currentLrc = match.captured(2);
|
||||||
|
match = lrcRegex.match(currentLrc);
|
||||||
|
}
|
||||||
|
if (!timestamps.isEmpty()) {
|
||||||
|
for (int timestamp : std::as_const(timestamps)) {
|
||||||
|
m_lyricsMap.insert(timestamp, currentLrc);
|
||||||
|
qCDebug(lcLyricsParser) << "[lrc]" << timestamp << currentLrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!m_lyricsMap.isEmpty()) {
|
||||||
|
m_timestampList = m_lyricsMap.keys();
|
||||||
|
std::sort(m_timestampList.begin(), m_timestampList.end());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LyricsManager::hasLyrics() const
|
||||||
|
{
|
||||||
|
return !m_lyricsMap.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return;
|
||||||
|
|
||||||
|
// TODO: we don't need to find from the top everytime the time is updated
|
||||||
|
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end(), [&curTimeMs, this](int timestamp) -> bool {
|
||||||
|
return (timestamp + m_timeOffset) > curTimeMs;
|
||||||
|
});
|
||||||
|
|
||||||
|
m_nextLyricsTime = iter == m_timestampList.end() ? totalTimeMs : *iter;
|
||||||
|
if (iter != m_timestampList.begin()) {
|
||||||
|
iter--;
|
||||||
|
}
|
||||||
|
m_currentLyricsTime = *iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LyricsManager::lyrics(int lineOffset) const
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return QString();
|
||||||
|
|
||||||
|
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
|
||||||
|
if (index >= 0 && index < m_timestampList.count()) {
|
||||||
|
int timestamp = m_timestampList.at(index);
|
||||||
|
return m_lyricsMap.value(timestamp);
|
||||||
|
} else {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double LyricsManager::maskPercent(int curTimeMs)
|
||||||
|
{
|
||||||
|
if (!hasLyrics()) return 0;
|
||||||
|
if (curTimeMs <= currentLyricsTime()) return 0;
|
||||||
|
if (curTimeMs >= nextLyricsTime()) return 1;
|
||||||
|
if (m_nextLyricsTime == currentLyricsTime()) return 1;
|
||||||
|
|
||||||
|
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::parseTimeToMilliseconds(const QString &timeString)
|
||||||
|
{
|
||||||
|
QRegularExpression timeRegex(R"((\d{2,3}):(\d{2})\.(\d{2,3}))");
|
||||||
|
QRegularExpressionMatch match = timeRegex.match(timeString);
|
||||||
|
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
int minutes = match.captured(1).toInt();
|
||||||
|
int seconds = match.captured(2).toInt();
|
||||||
|
int milliseconds = match.captured(3).toInt();
|
||||||
|
|
||||||
|
return minutes * 60000 + seconds * 1000 + milliseconds;
|
||||||
|
} else {
|
||||||
|
qCWarning(lcLyricsParser) << "Invalid time format:" << timeString;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsManager::reset()
|
||||||
|
{
|
||||||
|
m_currentLyricsTime = 0;
|
||||||
|
m_nextLyricsTime = 0;
|
||||||
|
m_timeOffset = 0;
|
||||||
|
m_lyricsMap.clear();
|
||||||
|
m_timestampList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::currentLyricsTime() const
|
||||||
|
{
|
||||||
|
return m_currentLyricsTime + m_timeOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::nextLyricsTime() const
|
||||||
|
{
|
||||||
|
return m_nextLyricsTime + m_timeOffset;
|
||||||
|
}
|
43
lyricsmanager.h
Normal file
43
lyricsmanager.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
|
||||||
|
|
||||||
|
class LyricsManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit LyricsManager(QObject *parent);
|
||||||
|
~LyricsManager();
|
||||||
|
|
||||||
|
bool loadLyrics(QString filepath);
|
||||||
|
bool hasLyrics() const;
|
||||||
|
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
|
||||||
|
QString lyrics(int lineOffset = 0) const;
|
||||||
|
double maskPercent(int curTimeMs);
|
||||||
|
|
||||||
|
static int parseTimeToMilliseconds(const QString& timeString);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
void reset();
|
||||||
|
int currentLyricsTime() const;
|
||||||
|
int nextLyricsTime() const;
|
||||||
|
|
||||||
|
QHash<int, QString> m_lyricsMap;
|
||||||
|
QList<int> m_timestampList;
|
||||||
|
int m_currentLyricsTime = 0;
|
||||||
|
int m_nextLyricsTime = 0;
|
||||||
|
int m_timeOffset = 0;
|
||||||
|
};
|
13
main.cpp
13
main.cpp
@ -1,3 +1,7 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
|
|
||||||
#include "singleapplicationmanager.h"
|
#include "singleapplicationmanager.h"
|
||||||
@ -14,14 +18,9 @@ int main(int argc, char *argv[])
|
|||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
QString qmDir;
|
if (translator.load(QLocale(), QLatin1String("pineapple-music"), QLatin1String("_"), QLatin1String(":/i18n"))) {
|
||||||
#ifdef _WIN32
|
|
||||||
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
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
|
625
mainwindow.cpp
625
mainwindow.cpp
@ -1,11 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "./ui_mainwindow.h"
|
#include "./ui_mainwindow.h"
|
||||||
|
|
||||||
#include "playlistmodel.h"
|
#include "playlistmanager.h"
|
||||||
#include "qt/qmediaplaylist.h"
|
#include "fftspectrum.h"
|
||||||
|
#include "lrcbar.h"
|
||||||
#include "ID3v2Pic.h"
|
#include "taskbarmanager.h"
|
||||||
#include "FlacPic.h"
|
|
||||||
|
|
||||||
// taglib
|
// taglib
|
||||||
#ifndef NO_TAGLIB
|
#ifndef NO_TAGLIB
|
||||||
@ -14,6 +17,7 @@
|
|||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
|
#include <QMediaMetaData>
|
||||||
#include <QAudioOutput>
|
#include <QAudioOutput>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
@ -23,30 +27,78 @@
|
|||||||
#include <QListView>
|
#include <QListView>
|
||||||
#include <QCollator>
|
#include <QCollator>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
#include <QMenu>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QMediaDevices>
|
||||||
|
#include <QAudioDevice>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QGraphicsDropShadowEffect>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
constexpr QSize miniSize(490, 160);
|
||||||
|
constexpr QSize fullSize(490, 420);
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
, ui(new Ui::MainWindow)
|
, ui(new Ui::MainWindow)
|
||||||
|
, m_mediaDevices(new QMediaDevices(this))
|
||||||
, m_mediaPlayer(new QMediaPlayer(this))
|
, m_mediaPlayer(new QMediaPlayer(this))
|
||||||
, m_audioOutput(new QAudioOutput(this))
|
, m_audioOutput(new QAudioOutput(this))
|
||||||
, m_playlistModel(new PlaylistModel(this))
|
, m_fftSpectrum(new FFTSpectrum(this))
|
||||||
|
, m_lrcbar(new LrcBar(nullptr))
|
||||||
|
, m_playlistManager(new PlaylistManager(this))
|
||||||
|
, m_taskbarManager(new TaskBarManager(this))
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
m_playlistManager->setAutoLoadFilterSuffixes({
|
||||||
|
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta"
|
||||||
|
});
|
||||||
|
m_fftSpectrum->setMediaPlayer(m_mediaPlayer);
|
||||||
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);
|
ui->chapterNameBtn->setVisible(false);
|
||||||
this->setAttribute(Qt::WA_TranslucentBackground, true);
|
ui->chapterlistView->setModel(ui->playbackProgressIndicator->chapterModel());
|
||||||
|
ui->chapterlistView->setRootIsDecorated(false);
|
||||||
|
|
||||||
|
ui->actionHelp->setShortcut(QKeySequence::HelpContents);
|
||||||
|
addAction(ui->actionHelp);
|
||||||
|
ui->actionOpen->setShortcut(QKeySequence::Open);
|
||||||
|
addAction(ui->actionOpen);
|
||||||
|
|
||||||
|
ui->titleLabel->setGraphicsEffect(createLabelShadowEffect());
|
||||||
|
ui->propLabel->setGraphicsEffect(createLabelShadowEffect());
|
||||||
|
ui->nowTimeLabel->setGraphicsEffect(createLabelShadowEffect());
|
||||||
|
ui->totalTimeLabel->setGraphicsEffect(createLabelShadowEffect());
|
||||||
|
|
||||||
|
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
|
||||||
|
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||||
|
|
||||||
|
m_taskbarManager->setCanTogglePlayback(true);
|
||||||
|
m_taskbarManager->setCanSkipBackward(true);
|
||||||
|
m_taskbarManager->setCanSkipForward(true);
|
||||||
|
m_taskbarManager->setShowProgress(true);
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
loadSkinData();
|
||||||
initConnections();
|
initConnections();
|
||||||
initUiAndAnimation();
|
initUiAndAnimation();
|
||||||
|
|
||||||
centerWindow();
|
centerWindow();
|
||||||
|
|
||||||
|
QTimer::singleShot(1000, [this](){
|
||||||
|
m_taskbarManager->setWinId(window()->winId());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
|
saveConfig();
|
||||||
|
delete m_lrcbar;
|
||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,45 +107,12 @@ void MainWindow::commandlinePlayAudioFiles(QStringList audioFiles)
|
|||||||
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
|
QList<QUrl> audioFileUrls = strlst2urllst(audioFiles);
|
||||||
|
|
||||||
if (!audioFileUrls.isEmpty()) {
|
if (!audioFileUrls.isEmpty()) {
|
||||||
if (audioFileUrls.count() == 1) {
|
QModelIndex modelIndex = m_playlistManager->loadPlaylist(audioFileUrls);
|
||||||
loadPlaylistBySingleLocalFile(audioFileUrls.first().toLocalFile());
|
if (modelIndex.isValid()) {
|
||||||
} else {
|
loadByModelIndex(modelIndex);
|
||||||
createPlaylist(audioFileUrls);
|
play();
|
||||||
}
|
|
||||||
m_mediaPlayer->play();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadPlaylistBySingleLocalFile(const QString &path)
|
|
||||||
{
|
|
||||||
QFileInfo info(path);
|
|
||||||
QDir dir(info.path());
|
|
||||||
QString currentFileName = info.fileName();
|
|
||||||
QStringList entryList = dir.entryList({"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga"},
|
|
||||||
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
|
||||||
|
|
||||||
QCollator collator;
|
|
||||||
collator.setNumericMode(true);
|
|
||||||
|
|
||||||
std::sort(entryList.begin(), entryList.end(), collator);
|
|
||||||
|
|
||||||
QList<QUrl> urlList;
|
|
||||||
int currentFileIndex = -1;
|
|
||||||
for (int i = 0; i < entryList.count(); i++) {
|
|
||||||
const QString & oneEntry = entryList.at(i);
|
|
||||||
urlList.append(QUrl::fromLocalFile(dir.absoluteFilePath(oneEntry)));
|
|
||||||
if (oneEntry == currentFileName) {
|
|
||||||
currentFileIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFileIndex == -1) {
|
|
||||||
// not in the list probably because of the suffix is not a common one, add it to the first one anyway.
|
|
||||||
urlList.prepend(QUrl::fromLocalFile(path));
|
|
||||||
currentFileIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
createPlaylist(urlList, currentFileIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt)
|
void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int channelCount, QString audioExt)
|
||||||
@ -112,12 +131,12 @@ void MainWindow::setAudioPropertyInfoForDisplay(int sampleRate, int bitrate, int
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (sampleRate >= 0) {
|
if (sampleRate > 0) {
|
||||||
uiStrs << QString("%1 Hz").arg(sampleRate);
|
uiStrs << QString("%1 Hz").arg(sampleRate);
|
||||||
tooltipStrs << tr("Sample Rate: %1 Hz").arg(sampleRate);
|
tooltipStrs << tr("Sample Rate: %1 Hz").arg(sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bitrate >= 0) {
|
if (bitrate > 0) {
|
||||||
uiStrs << QString("%1 Kbps").arg(bitrate);
|
uiStrs << QString("%1 Kbps").arg(bitrate);
|
||||||
tooltipStrs << tr("Bitrate: %1 Kbps").arg(bitrate);
|
tooltipStrs << tr("Bitrate: %1 Kbps").arg(bitrate);
|
||||||
}
|
}
|
||||||
@ -169,10 +188,14 @@ void MainWindow::paintEvent(QPaintEvent * e)
|
|||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
|
|
||||||
painter.setPen(Qt::NoPen);
|
painter.setPen(Qt::NoPen);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||||
|
|
||||||
// Temp bg
|
if (m_skin.isNull()) {
|
||||||
painter.setBrush(QColor(20, 32, 83));
|
painter.setBrush(QColor(40, 50, 123));
|
||||||
painter.drawRect(0, 0, width(), height());
|
painter.drawRect(0, 0, width(), height());
|
||||||
|
} else {
|
||||||
|
painter.drawPixmap(0, 0, m_skin);
|
||||||
|
}
|
||||||
|
|
||||||
painter.setBrush(QBrush(m_bgLinearGradient));
|
painter.setBrush(QBrush(m_bgLinearGradient));
|
||||||
painter.drawRect(0, 0, width(), height());
|
painter.drawRect(0, 0, width(), height());
|
||||||
@ -193,7 +216,6 @@ void MainWindow::mousePressEvent(QMouseEvent *event)
|
|||||||
void MainWindow::mouseMoveEvent(QMouseEvent *event)
|
void MainWindow::mouseMoveEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (event->buttons() & Qt::LeftButton && m_clickedOnWindow) {
|
if (event->buttons() & Qt::LeftButton && m_clickedOnWindow) {
|
||||||
qDebug() << "??" << event << event->flags() << event->isBeginEvent() << event->isEndEvent();
|
|
||||||
window()->windowHandle()->startSystemMove();
|
window()->windowHandle()->startSystemMove();
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
@ -204,7 +226,6 @@ void MainWindow::mouseMoveEvent(QMouseEvent *event)
|
|||||||
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
|
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
m_clickedOnWindow = false;
|
m_clickedOnWindow = false;
|
||||||
qDebug() << "?";
|
|
||||||
return QMainWindow::mouseReleaseEvent(event);
|
return QMainWindow::mouseReleaseEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,56 +248,94 @@ void MainWindow::dropEvent(QDropEvent *e)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: file/format filter?
|
if (fileName.endsWith(".png") || fileName.endsWith(".jpg") ||
|
||||||
|
fileName.endsWith(".jpeg") || fileName.endsWith(".gif")) {
|
||||||
|
setSkin(fileName, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createPlaylist(urls);
|
if (fileName.endsWith(".lrc")) {
|
||||||
m_mediaPlayer->play();
|
m_lrcbar->loadLyrics(fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName.endsWith(".chp") || fileName.endsWith(".pbf")) {
|
||||||
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(fileName));
|
||||||
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName.endsWith(".m3u") || fileName.endsWith(".m3u8")) {
|
||||||
|
const QModelIndex & modelIndex = m_playlistManager->loadM3U8Playlist(urls.constFirst());
|
||||||
|
if (modelIndex.isValid()) {
|
||||||
|
loadByModelIndex(modelIndex);
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||||
|
if (modelIndex.isValid()) {
|
||||||
|
loadByModelIndex(modelIndex);
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
|
||||||
|
{
|
||||||
|
QMenu * menu = new QMenu;
|
||||||
|
menu->addAction(ui->actionHelp);
|
||||||
|
menu->exec(mapToGlobal(event->pos()));
|
||||||
|
menu->deleteLater();
|
||||||
|
|
||||||
|
return QMainWindow::contextMenuEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadFile()
|
void MainWindow::loadFile()
|
||||||
{
|
{
|
||||||
|
QStringList musicFolders(QStandardPaths::standardLocations(QStandardPaths::MusicLocation));
|
||||||
|
musicFolders.append(QDir::homePath());
|
||||||
QStringList files = QFileDialog::getOpenFileNames(this,
|
QStringList files = QFileDialog::getOpenFileNames(this,
|
||||||
tr("Select songs to play"),
|
tr("Select songs to play"),
|
||||||
QDir::homePath(),
|
musicFolders.first(),
|
||||||
tr("Audio Files") + " (*.mp3 *.wav *.aiff *.ape *.flac *.ogg *.oga)");
|
tr("Audio Files") + " (*.mp3 *.wav *.aiff *.ape *.flac *.ogg *.oga)");
|
||||||
|
if (files.isEmpty()) return;
|
||||||
QList<QUrl> urlList;
|
QList<QUrl> urlList;
|
||||||
for (const QString & fileName : files) {
|
for (const QString & fileName : files) {
|
||||||
urlList.append(QUrl::fromLocalFile(fileName));
|
urlList.append(QUrl::fromLocalFile(fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
createPlaylist(urlList);
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urlList);
|
||||||
|
loadByModelIndex(modelIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
void MainWindow::loadFile(const QUrl &url)
|
||||||
* The returned QMediaPlaylist* ownership belongs to the internal QMediaPlayer instance.
|
|
||||||
*/
|
|
||||||
void MainWindow::createPlaylist(QList<QUrl> urlList, int index)
|
|
||||||
{
|
{
|
||||||
QMediaPlaylist* playlist = m_playlistModel->playlist();
|
const QString filePath = url.toLocalFile();
|
||||||
playlist->clear();
|
m_mediaPlayer->setSource(url);
|
||||||
playlist->addMedia(urlList);
|
m_lrcbar->loadLyrics(filePath);
|
||||||
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath));
|
||||||
connect(playlist, &QMediaPlaylist::playbackModeChanged, this, [=](QMediaPlaylist::PlaybackMode mode) {
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
switch (mode) {
|
|
||||||
case QMediaPlaylist::CurrentItemInLoop:
|
|
||||||
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png"));
|
|
||||||
break;
|
|
||||||
case QMediaPlaylist::Loop:
|
|
||||||
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png"));
|
|
||||||
break;
|
|
||||||
case QMediaPlaylist::Sequential:
|
|
||||||
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-normal.png"));
|
|
||||||
break;
|
|
||||||
// case QMediaPlaylist::Random:
|
|
||||||
// ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-shuffle.png"));
|
|
||||||
// break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
|
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
||||||
playlist->setCurrentIndex(index < 0 ? 0 : index);
|
{
|
||||||
|
loadFile(m_playlistManager->urlByIndex(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::play()
|
||||||
|
{
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::setSkin(QString imagePath, bool save)
|
||||||
|
{
|
||||||
|
m_skin = QPixmap(imagePath);
|
||||||
|
if (save) {
|
||||||
|
saveSkinData();
|
||||||
|
}
|
||||||
|
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::centerWindow()
|
void MainWindow::centerWindow()
|
||||||
@ -300,7 +359,7 @@ void MainWindow::on_playBtn_clicked()
|
|||||||
{
|
{
|
||||||
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
|
if (m_mediaPlayer->mediaStatus() == QMediaPlayer::NoMedia) {
|
||||||
loadFile();
|
loadFile();
|
||||||
m_mediaPlayer->play();
|
play();
|
||||||
} else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) {
|
} else if (m_mediaPlayer->mediaStatus() == QMediaPlayer::InvalidMedia) {
|
||||||
ui->propLabel->setText("Error: InvalidMedia" + m_mediaPlayer->errorString());
|
ui->propLabel->setText("Error: InvalidMedia" + m_mediaPlayer->errorString());
|
||||||
} else {
|
} else {
|
||||||
@ -313,16 +372,6 @@ void MainWindow::on_playBtn_clicked()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::ms2str(qint64 ms)
|
|
||||||
{
|
|
||||||
QTime duaTime(QTime::fromMSecsSinceStartOfDay(ms));
|
|
||||||
if (duaTime.hour() > 0) {
|
|
||||||
return duaTime.toString("h:mm:ss");
|
|
||||||
} else {
|
|
||||||
return duaTime.toString("m:ss");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QUrl> MainWindow::strlst2urllst(QStringList strlst)
|
QList<QUrl> MainWindow::strlst2urllst(QStringList strlst)
|
||||||
{
|
{
|
||||||
QList<QUrl> urlList;
|
QList<QUrl> urlList;
|
||||||
@ -349,37 +398,20 @@ void MainWindow::on_stopBtn_clicked()
|
|||||||
m_mediaPlayer->stop();
|
m_mediaPlayer->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_playbackSlider_valueChanged(int value)
|
|
||||||
{
|
|
||||||
qint64 currPos = m_mediaPlayer->duration() == 0 ? value : m_mediaPlayer->position() * ui->playbackSlider->maximum() / m_mediaPlayer->duration();
|
|
||||||
if (qAbs(currPos - value) > 2) {
|
|
||||||
m_mediaPlayer->setPosition(ui->playbackSlider->value() * 1.0 / ui->playbackSlider->maximum() * m_mediaPlayer->duration());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::on_prevBtn_clicked()
|
void MainWindow::on_prevBtn_clicked()
|
||||||
{
|
{
|
||||||
// QMediaPlaylist::previous() won't work when in CurrentItemInLoop playmode,
|
QModelIndex index(m_playlistManager->previousIndex());
|
||||||
// and also works not as intended when in other playmode, so do it manually...
|
m_playlistManager->setCurrentIndex(index);
|
||||||
QMediaPlaylist * playlist = m_playlistModel->playlist();
|
loadByModelIndex(index);
|
||||||
if (playlist) {
|
play();
|
||||||
int index = playlist->currentIndex();
|
|
||||||
int count = playlist->mediaCount();
|
|
||||||
|
|
||||||
playlist->setCurrentIndex(index == 0 ? count - 1 : index - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_nextBtn_clicked()
|
void MainWindow::on_nextBtn_clicked()
|
||||||
{
|
{
|
||||||
// see also: MainWindow::on_prevBtn_clicked()
|
QModelIndex index(m_playlistManager->nextIndex());
|
||||||
QMediaPlaylist * playlist = m_playlistModel->playlist();
|
m_playlistManager->setCurrentIndex(index);
|
||||||
if (playlist) {
|
loadByModelIndex(index);
|
||||||
int index = playlist->currentIndex();
|
play();
|
||||||
int count = playlist->mediaCount();
|
|
||||||
|
|
||||||
playlist->setCurrentIndex(index == (count - 1) ? 0 : index + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_volumeBtn_clicked()
|
void MainWindow::on_volumeBtn_clicked()
|
||||||
@ -394,8 +426,8 @@ void MainWindow::on_minimumWindowBtn_clicked()
|
|||||||
|
|
||||||
void MainWindow::initUiAndAnimation()
|
void MainWindow::initUiAndAnimation()
|
||||||
{
|
{
|
||||||
m_bgLinearGradient.setColorAt(0, QColor(255, 255, 255, 25)); // a:0
|
m_bgLinearGradient.setColorAt(0, QColor(0, 0, 0, 25));
|
||||||
m_bgLinearGradient.setColorAt(1, QColor(255, 255, 255, 75)); // a:200
|
m_bgLinearGradient.setColorAt(1, QColor(0, 0, 0, 80));
|
||||||
m_bgLinearGradient.setStart(0, 0);
|
m_bgLinearGradient.setStart(0, 0);
|
||||||
m_bgLinearGradient.setFinalStop(0, height());
|
m_bgLinearGradient.setFinalStop(0, height());
|
||||||
|
|
||||||
@ -404,22 +436,22 @@ 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(miniSize);
|
||||||
// temp: a playlist for debug...
|
|
||||||
QListView * tmp_listview = new QListView(ui->pluginWidget);
|
|
||||||
tmp_listview->setModel(m_playlistModel);
|
|
||||||
tmp_listview->setGeometry({0,0,490,250});
|
|
||||||
this->setGeometry({0,0,490,160}); // temp size, hide the playlist thing.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::initConnections()
|
void MainWindow::initConnections()
|
||||||
{
|
{
|
||||||
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentIndexChanged, this, [=](int currentItem) {
|
connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, [=]{
|
||||||
bool isPlaying = m_mediaPlayer->playbackState() == QMediaPlayer::PlayingState;
|
m_audioOutput->setDevice(m_mediaDevices->defaultAudioOutput());
|
||||||
m_mediaPlayer->setSource(m_playlistModel->playlist()->currentMedia());
|
|
||||||
if (isPlaying) m_mediaPlayer->play();
|
|
||||||
});
|
});
|
||||||
connect(m_playlistModel->playlist(), &QMediaPlaylist::currentMediaChanged, this, [=](const QUrl &fileUrl) {
|
|
||||||
|
connect(ui->playbackProgressIndicator, &PlaybackProgressIndicator::seekingRequested, this, [=](qint64 pos){
|
||||||
|
m_mediaPlayer->setPosition(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::sourceChanged, this, [=](){
|
||||||
|
QUrl fileUrl(m_mediaPlayer->source());
|
||||||
|
|
||||||
ui->titleLabel->setText(fileUrl.fileName());
|
ui->titleLabel->setText(fileUrl.fileName());
|
||||||
ui->titleLabel->setToolTip(fileUrl.fileName());
|
ui->titleLabel->setToolTip(fileUrl.fileName());
|
||||||
|
|
||||||
@ -434,6 +466,8 @@ void MainWindow::initConnections()
|
|||||||
if (!fileRef.isNull() && fileRef.audioProperties()) {
|
if (!fileRef.isNull() && fileRef.audioProperties()) {
|
||||||
TagLib::AudioProperties *prop = fileRef.audioProperties();
|
TagLib::AudioProperties *prop = fileRef.audioProperties();
|
||||||
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
|
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
|
||||||
|
} else {
|
||||||
|
qDebug() << "No Audio Properties from TagLib";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileRef.isNull() && fileRef.tag()) {
|
if (!fileRef.isNull() && fileRef.tag()) {
|
||||||
@ -441,40 +475,66 @@ void MainWindow::initConnections()
|
|||||||
setAudioMetadataForDisplay(QString::fromStdString(tag->title().to8Bit(true)),
|
setAudioMetadataForDisplay(QString::fromStdString(tag->title().to8Bit(true)),
|
||||||
QString::fromStdString(tag->artist().to8Bit(true)),
|
QString::fromStdString(tag->artist().to8Bit(true)),
|
||||||
QString::fromStdString(tag->album().to8Bit(true)));
|
QString::fromStdString(tag->album().to8Bit(true)));
|
||||||
|
m_urlMissingTagLibMetadata.clear();
|
||||||
|
} else {
|
||||||
|
qDebug() << "No Audio Metadata from TagLib";
|
||||||
|
m_urlMissingTagLibMetadata = fileUrl;
|
||||||
}
|
}
|
||||||
#endif // NO_TAGLIB
|
#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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, [=](){
|
||||||
|
QMediaMetaData metadata(m_mediaPlayer->metaData());
|
||||||
|
// it's known in some cases QMediaMetaData using the incorrect text codec for metadata
|
||||||
|
// see `02 Yoiyami Hanabi.mp3`'s Title. So we don't use Qt's one if tablib is available.
|
||||||
|
qDebug() << metadata.stringValue(QMediaMetaData::Title) << metadata.stringValue(QMediaMetaData::Author);
|
||||||
|
#ifdef NO_TAGLIB
|
||||||
|
bool needMetadataFromQt = true;
|
||||||
|
#else
|
||||||
|
bool needMetadataFromQt = m_urlMissingTagLibMetadata == m_mediaPlayer->source();
|
||||||
|
#endif // NO_TAGLIB
|
||||||
|
if (needMetadataFromQt) {
|
||||||
|
setAudioMetadataForDisplay(metadata.stringValue(QMediaMetaData::Title),
|
||||||
|
metadata.stringValue(QMediaMetaData::Author),
|
||||||
|
metadata.stringValue(QMediaMetaData::AlbumTitle));
|
||||||
|
setAudioPropertyInfoForDisplay(-1, metadata.value(QMediaMetaData::AudioBitRate).toInt() / 1000,
|
||||||
|
-1, metadata.stringValue(QMediaMetaData::FileFormat));
|
||||||
|
}
|
||||||
|
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
||||||
|
if (!coverArt.isNull()) {
|
||||||
|
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
|
||||||
|
} else {
|
||||||
|
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
||||||
|
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
|
||||||
|
ui->playlistView->setCurrentIndex(m_playlistManager->model()->index(index));
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
|
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
|
||||||
ui->nowTimeLabel->setText(ms2str(pos));
|
ui->nowTimeLabel->setText(PlaybackProgressIndicator::formatTime(pos));
|
||||||
if (m_mediaPlayer->duration() != 0) {
|
if (m_mediaPlayer->duration() != 0) {
|
||||||
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
|
ui->playbackProgressIndicator->setPosition(pos);
|
||||||
|
m_taskbarManager->setProgressValue(pos);
|
||||||
|
}
|
||||||
|
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
|
||||||
|
|
||||||
|
static QString lastChapterName;
|
||||||
|
if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) {
|
||||||
|
QString currentChapterName = ui->playbackProgressIndicator->currentChapterName();
|
||||||
|
if (currentChapterName != lastChapterName) {
|
||||||
|
ui->chapterNameBtn->setText(currentChapterName);
|
||||||
|
lastChapterName = currentChapterName;
|
||||||
|
}
|
||||||
|
ui->chapterNameBtn->setVisible(true);
|
||||||
|
} else {
|
||||||
|
if (!lastChapterName.isEmpty()) {
|
||||||
|
ui->chapterNameBtn->setText("");
|
||||||
|
lastChapterName.clear();
|
||||||
|
}
|
||||||
|
ui->chapterNameBtn->setVisible(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -487,7 +547,9 @@ void MainWindow::initConnections()
|
|||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, [=](qint64 dua) {
|
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, [=](qint64 dua) {
|
||||||
ui->totalTimeLabel->setText(ms2str(dua));
|
ui->playbackProgressIndicator->setDuration(dua);
|
||||||
|
m_taskbarManager->setProgressMaximum(dua);
|
||||||
|
ui->totalTimeLabel->setText(PlaybackProgressIndicator::formatTime(dua));
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, [=](QMediaPlayer::PlaybackState newState) {
|
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, [=](QMediaPlayer::PlaybackState newState) {
|
||||||
@ -500,41 +562,240 @@ void MainWindow::initConnections()
|
|||||||
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
m_taskbarManager->setPlaybackState(newState);
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_audioOutput, &QAudioOutput::volumeChanged, this, [=](float vol) {
|
connect(m_audioOutput, &QAudioOutput::volumeChanged, this, [=](float vol) {
|
||||||
ui->volumeSlider->setValue(vol * 100);
|
ui->volumeSlider->setValue(vol * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// connect(m_mediaPlayer, static_cast<void(QMediaPlayer::*)(QMediaPlayer::Error)>(&QMediaPlayer::error),
|
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, [=](QMediaPlayer::MediaStatus status){
|
||||||
// this, [=](QMediaPlayer::Error error) {
|
if (status == QMediaPlayer::EndOfMedia) {
|
||||||
// switch (error) {
|
switch (m_playbackMode) {
|
||||||
// default:
|
case MainWindow::CurrentItemOnce:
|
||||||
// break;
|
// do nothing
|
||||||
// }
|
break;
|
||||||
// qDebug("%s aaaaaaaaaaaaa", m_mediaPlayer->errorString().toUtf8().data());
|
case MainWindow::CurrentItemInLoop:
|
||||||
// });
|
// also do nothing
|
||||||
|
// as long as we did `setLoops(Infinite)`, we won't even get there
|
||||||
|
break;
|
||||||
|
case MainWindow::Sequential:
|
||||||
|
on_nextBtn_clicked();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(this, &MainWindow::playbackModeChanged, this, [=](){
|
||||||
|
switch (m_playbackMode) {
|
||||||
|
case MainWindow::CurrentItemOnce:
|
||||||
|
m_mediaPlayer->setLoops(QMediaPlayer::Once);
|
||||||
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-repeat-single.png"));
|
||||||
|
break;
|
||||||
|
case MainWindow::CurrentItemInLoop:
|
||||||
|
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
||||||
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat-song.png"));
|
||||||
|
break;
|
||||||
|
case MainWindow::Sequential:
|
||||||
|
m_mediaPlayer->setLoops(QMediaPlayer::Once);
|
||||||
|
ui->playbackModeBtn->setIcon(QIcon(":/icons/icons/media-playlist-repeat.png"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_taskbarManager, &TaskBarManager::togglePlayback, this, [this](){
|
||||||
|
on_playBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_taskbarManager, &TaskBarManager::skipBackward, this, [this](){
|
||||||
|
on_prevBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_taskbarManager, &TaskBarManager::skipForward, this, [this](){
|
||||||
|
on_nextBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
|
||||||
|
qDebug() << error << errorString;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadConfig()
|
||||||
|
{
|
||||||
|
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
|
||||||
|
QSettings settings(configDir.filePath("settings.ini"), QSettings::IniFormat);
|
||||||
|
ui->volumeSlider->setValue(settings.value("volume", 100).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::saveConfig()
|
||||||
|
{
|
||||||
|
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
|
||||||
|
if (!configDir.exists()) {
|
||||||
|
configDir.mkpath(".");
|
||||||
|
}
|
||||||
|
QSettings settings(configDir.filePath("settings.ini"), QSettings::IniFormat);
|
||||||
|
settings.setValue("volume", ui->volumeSlider->value());
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadSkinData()
|
||||||
|
{
|
||||||
|
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
|
||||||
|
QFile file(configDir.filePath("skin.dat"));
|
||||||
|
bool canOpen = file.open(QIODevice::ReadOnly);
|
||||||
|
if (!canOpen) return;
|
||||||
|
QDataStream stream(&file);
|
||||||
|
quint32 magic;
|
||||||
|
stream >> magic;
|
||||||
|
if (magic == 0x78297000) {
|
||||||
|
stream >> m_skin;
|
||||||
|
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::saveSkinData()
|
||||||
|
{
|
||||||
|
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
|
||||||
|
if (!configDir.exists()) {
|
||||||
|
configDir.mkpath(".");
|
||||||
|
}
|
||||||
|
QFile file(configDir.absoluteFilePath("skin.dat"));
|
||||||
|
file.open(QIODevice::WriteOnly);
|
||||||
|
QDataStream stream(&file);
|
||||||
|
stream << (quint32)0x78297000 << m_skin;
|
||||||
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_playbackModeBtn_clicked()
|
void MainWindow::on_playbackModeBtn_clicked()
|
||||||
{
|
{
|
||||||
QMediaPlaylist * playlist = m_playlistModel->playlist();
|
switch (m_playbackMode) {
|
||||||
if (!playlist) return;
|
case MainWindow::CurrentItemOnce:
|
||||||
|
setProperty("playbackMode", MainWindow::CurrentItemInLoop);
|
||||||
switch (playlist->playbackMode()) {
|
|
||||||
case QMediaPlaylist::CurrentItemInLoop:
|
|
||||||
playlist->setPlaybackMode(QMediaPlaylist::Loop);
|
|
||||||
break;
|
break;
|
||||||
case QMediaPlaylist::Loop:
|
case MainWindow::CurrentItemInLoop:
|
||||||
playlist->setPlaybackMode(QMediaPlaylist::Sequential);
|
setProperty("playbackMode", MainWindow::Sequential);
|
||||||
break;
|
break;
|
||||||
case QMediaPlaylist::Sequential:
|
case MainWindow::Sequential:
|
||||||
playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
|
setProperty("playbackMode", MainWindow::CurrentItemOnce);
|
||||||
break;
|
break;
|
||||||
// case QMediaPlaylist::Random:
|
|
||||||
// playlist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
|
|
||||||
// break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_setSkinBtn_clicked()
|
||||||
|
{
|
||||||
|
QStringList imageFolders(QStandardPaths::standardLocations(QStandardPaths::PicturesLocation));
|
||||||
|
imageFolders.append(QDir::homePath());
|
||||||
|
QString image = QFileDialog::getOpenFileName(this, tr("Select image as background skin"),
|
||||||
|
imageFolders.first(),
|
||||||
|
tr("Image files (*.jpg *.jpeg *.png *.gif)"));
|
||||||
|
if(!image.isEmpty()) {
|
||||||
|
setSkin(image, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_playListBtn_clicked()
|
||||||
|
{
|
||||||
|
if (size().height() < 200) {
|
||||||
|
setFixedSize(fullSize);
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->playlistViewPage);
|
||||||
|
} else {
|
||||||
|
if (ui->pluginStackedWidget->currentWidget() == ui->playlistViewPage) {
|
||||||
|
setFixedSize(miniSize);
|
||||||
|
} else {
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->playlistViewPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
m_playlistManager->setCurrentIndex(index);
|
||||||
|
loadByModelIndex(index);
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_lrcBtn_clicked()
|
||||||
|
{
|
||||||
|
if (m_lrcbar->isVisible()) {
|
||||||
|
m_lrcbar->hide();
|
||||||
|
} else {
|
||||||
|
m_lrcbar->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_chapterlistView_activated(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
if (!index.isValid()) return;
|
||||||
|
|
||||||
|
QModelIndex timeColumnIndex = index.sibling(index.row(), 0);
|
||||||
|
QStandardItem* timeItem = ui->playbackProgressIndicator->chapterModel()->itemFromIndex(timeColumnIndex);
|
||||||
|
if (!timeItem) return;
|
||||||
|
|
||||||
|
qint64 chapterStartTime = timeItem->data(PlaybackProgressIndicator::StartTimeMsRole).toLongLong();
|
||||||
|
m_mediaPlayer->setPosition(chapterStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_chapterNameBtn_clicked()
|
||||||
|
{
|
||||||
|
if (size().height() < 200) {
|
||||||
|
setFixedSize(fullSize);
|
||||||
|
}
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->chaptersViewPage);
|
||||||
|
if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) {
|
||||||
|
const QModelIndex & curChapterItem = ui->playbackProgressIndicator->currentChapterItem();
|
||||||
|
if (curChapterItem.isValid()) {
|
||||||
|
ui->chapterlistView->setCurrentIndex(curChapterItem);
|
||||||
|
ui->chapterlistView->scrollTo(curChapterItem, QAbstractItemView::EnsureVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_actionOpen_triggered()
|
||||||
|
{
|
||||||
|
loadFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_actionHelp_triggered()
|
||||||
|
{
|
||||||
|
QMessageBox infoBox(this);
|
||||||
|
infoBox.setIcon(QMessageBox::Information);
|
||||||
|
infoBox.setWindowTitle(tr("About"));
|
||||||
|
infoBox.setStandardButtons(QMessageBox::Ok);
|
||||||
|
infoBox.setText(
|
||||||
|
tr("Pineapple Music") %
|
||||||
|
"\n\n" %
|
||||||
|
tr("Based on the following free software libraries:") %
|
||||||
|
"\n\n" %
|
||||||
|
QStringLiteral("- [Qt](https://www.qt.io/) %1 with the following module(s):\n").arg(QT_VERSION_STR) %
|
||||||
|
QStringLiteral(" - multimedia\n") %
|
||||||
|
#ifdef USE_QTEXTCODEC
|
||||||
|
QStringLiteral(" - core5compat\n") %
|
||||||
|
#endif
|
||||||
|
#ifndef NO_TAGLIB
|
||||||
|
QStringLiteral("- [TagLib](https://github.com/taglib/taglib)\n") %
|
||||||
|
#endif // NO_TAGLIB
|
||||||
|
#ifdef HAVE_KCODECS
|
||||||
|
QStringLiteral("- [KCodecs](https://invent.kde.org/frameworks/kcodecs)\n") %
|
||||||
|
#endif // NO_TAGLIB
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
QStringLiteral("- [FFmpeg](https://ffmpeg.org/)\n") %
|
||||||
|
#endif // HAVE_FFMPEG
|
||||||
|
"\n"
|
||||||
|
"[Source Code](https://github.com/BLumia/pineapple-music)\n"
|
||||||
|
"\n"
|
||||||
|
"Copyright © 2025 [BLumia](https://github.com/BLumia/)"
|
||||||
|
);
|
||||||
|
infoBox.setTextFormat(Qt::MarkdownText);
|
||||||
|
infoBox.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsDropShadowEffect *MainWindow::createLabelShadowEffect()
|
||||||
|
{
|
||||||
|
QGraphicsDropShadowEffect * effect = new QGraphicsDropShadowEffect(this);
|
||||||
|
effect->setBlurRadius(3);
|
||||||
|
effect->setColor(QColor(0, 0, 0, 180));
|
||||||
|
effect->setOffset(1, 1);
|
||||||
|
return effect;
|
||||||
|
}
|
||||||
|
58
mainwindow.h
58
mainwindow.h
@ -1,28 +1,46 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#ifndef MAINWINDOW_H
|
#ifndef MAINWINDOW_H
|
||||||
#define MAINWINDOW_H
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
namespace Ui { class MainWindow; }
|
namespace Ui { class MainWindow; }
|
||||||
|
|
||||||
|
class QMediaDevices;
|
||||||
class QMediaPlayer;
|
class QMediaPlayer;
|
||||||
class QAudioOutput;
|
class QAudioOutput;
|
||||||
class QPropertyAnimation;
|
class QPropertyAnimation;
|
||||||
|
class QGraphicsDropShadowEffect;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class PlaylistModel;
|
class FFTSpectrum;
|
||||||
|
class LrcBar;
|
||||||
|
class PlaylistManager;
|
||||||
|
class TaskBarManager;
|
||||||
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);
|
||||||
|
|
||||||
@ -37,10 +55,16 @@ protected:
|
|||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent *e) override;
|
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||||
void dropEvent(QDropEvent *e) override;
|
void dropEvent(QDropEvent *e) override;
|
||||||
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
|
||||||
void loadFile();
|
void loadFile();
|
||||||
|
void loadFile(const QUrl &url);
|
||||||
|
void loadByModelIndex(const QModelIndex &index);
|
||||||
|
void play();
|
||||||
|
|
||||||
|
void setSkin(QString imagePath, bool save);
|
||||||
|
|
||||||
void centerWindow();
|
void centerWindow();
|
||||||
void createPlaylist(QList<QUrl> urlList, int index = -1);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void on_playbackModeBtn_clicked();
|
void on_playbackModeBtn_clicked();
|
||||||
@ -48,27 +72,51 @@ private slots:
|
|||||||
void on_playBtn_clicked();
|
void on_playBtn_clicked();
|
||||||
void on_volumeSlider_valueChanged(int value);
|
void on_volumeSlider_valueChanged(int value);
|
||||||
void on_stopBtn_clicked();
|
void on_stopBtn_clicked();
|
||||||
void on_playbackSlider_valueChanged(int value);
|
|
||||||
void on_prevBtn_clicked();
|
void on_prevBtn_clicked();
|
||||||
void on_nextBtn_clicked();
|
void on_nextBtn_clicked();
|
||||||
void on_volumeBtn_clicked();
|
void on_volumeBtn_clicked();
|
||||||
void on_minimumWindowBtn_clicked();
|
void on_minimumWindowBtn_clicked();
|
||||||
|
void on_setSkinBtn_clicked();
|
||||||
|
void on_playListBtn_clicked();
|
||||||
|
void on_playlistView_activated(const QModelIndex &index);
|
||||||
|
void on_lrcBtn_clicked();
|
||||||
|
void on_chapterlistView_activated(const QModelIndex &index);
|
||||||
|
void on_chapterNameBtn_clicked();
|
||||||
|
void on_actionOpen_triggered();
|
||||||
|
void on_actionHelp_triggered();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void playbackModeChanged(enum PlaybackMode mode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QGraphicsDropShadowEffect * createLabelShadowEffect();
|
||||||
|
|
||||||
bool m_clickedOnWindow = false;
|
bool m_clickedOnWindow = false;
|
||||||
bool m_playbackSliderPressed = false;
|
bool m_playbackSliderPressed = false;
|
||||||
QLinearGradient m_bgLinearGradient;
|
QLinearGradient m_bgLinearGradient;
|
||||||
|
QPixmap m_skin;
|
||||||
|
QUrl m_urlMissingTagLibMetadata;
|
||||||
|
enum PlaybackMode m_playbackMode = CurrentItemInLoop;
|
||||||
|
|
||||||
Ui::MainWindow *ui;
|
Ui::MainWindow *ui;
|
||||||
|
|
||||||
|
QMediaDevices *m_mediaDevices;
|
||||||
QMediaPlayer *m_mediaPlayer;
|
QMediaPlayer *m_mediaPlayer;
|
||||||
QAudioOutput *m_audioOutput;
|
QAudioOutput *m_audioOutput;
|
||||||
|
FFTSpectrum* m_fftSpectrum;
|
||||||
|
LrcBar *m_lrcbar;
|
||||||
QPropertyAnimation *m_fadeOutAnimation;
|
QPropertyAnimation *m_fadeOutAnimation;
|
||||||
PlaylistModel *m_playlistModel = nullptr; // TODO: move playback logic to player.cpp
|
PlaylistManager *m_playlistManager;
|
||||||
|
TaskBarManager *m_taskbarManager;
|
||||||
|
|
||||||
void initUiAndAnimation();
|
void initUiAndAnimation();
|
||||||
void initConnections();
|
void initConnections();
|
||||||
|
|
||||||
|
void loadConfig();
|
||||||
|
void saveConfig();
|
||||||
|
void loadSkinData();
|
||||||
|
void saveSkinData();
|
||||||
|
|
||||||
static QString ms2str(qint64 ms);
|
static QString ms2str(qint64 ms);
|
||||||
static QList<QUrl> strlst2urllst(QStringList strlst);
|
static QList<QUrl> strlst2urllst(QStringList strlst);
|
||||||
};
|
};
|
||||||
|
183
mainwindow.ui
183
mainwindow.ui
@ -20,7 +20,7 @@
|
|||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Pineapple Player</string>
|
<string>Pineapple Music</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="resources.qrc">
|
<iconset resource="resources.qrc">
|
||||||
@ -72,16 +72,17 @@ QSlider::add-page:vertical {
|
|||||||
/****** PushButton ******/
|
/****** PushButton ******/
|
||||||
|
|
||||||
QPushButton {
|
QPushButton {
|
||||||
|
color: white;
|
||||||
border: 0px solid grey;
|
border: 0px solid grey;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: rgba(255, 255, 255, 220);
|
background-color: rgba(0, 85, 255, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background-color: rgba(255, 255, 255, 250);
|
background-color: rgba(0, 85, 255, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#closeWindowBtn {
|
QPushButton#closeWindowBtn {
|
||||||
@ -101,6 +102,25 @@ QLabel {
|
|||||||
|
|
||||||
QLabel#coverLabel {
|
QLabel#coverLabel {
|
||||||
border: 1px solid grey;
|
border: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/****** ListView ******/
|
||||||
|
|
||||||
|
QListView {
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****** TreeView ******/
|
||||||
|
|
||||||
|
QTreeView {
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(200, 200, 200, 50);
|
||||||
}</string>
|
}</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="locale">
|
<property name="locale">
|
||||||
@ -115,7 +135,7 @@ QLabel#coverLabel {
|
|||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
|
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>7</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
@ -220,7 +240,7 @@ QLabel#coverLabel {
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="miniModeBtn">
|
<widget class="QPushButton" name="setSkinBtn">
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>25</width>
|
<width>25</width>
|
||||||
@ -228,7 +248,17 @@ QLabel#coverLabel {
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>^</string>
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/skin.png</normaloff>:/icons/icons/skin.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -298,6 +328,11 @@ QLabel#coverLabel {
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="propLabel">
|
<widget class="QLabel" name="propLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -305,19 +340,53 @@ QLabel#coverLabel {
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="lrcBtn">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string comment="Lyrics">Lrc</string>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="playbackTimeLayout">
|
<layout class="QHBoxLayout" name="playbackTimeLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="nowTimeLabel">
|
<widget class="QLabel" name="nowTimeLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>0:00</string>
|
<string notr="true">0:00</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="chapterNameBtn">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="totalTimeLabel">
|
<widget class="QLabel" name="totalTimeLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>0:00</string>
|
<string notr="true">0:00</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
@ -327,14 +396,7 @@ QLabel#coverLabel {
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="SeekableSlider" name="playbackSlider">
|
<widget class="PlaybackProgressIndicator" name="playbackProgressIndicator" native="true"/>
|
||||||
<property name="maximum">
|
|
||||||
<number>1000</number>
|
|
||||||
</property>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="playbackControlLayout">
|
<layout class="QHBoxLayout" name="playbackControlLayout">
|
||||||
@ -576,9 +638,9 @@ QLabel#coverLabel {
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QWidget" name="pluginWidget" native="true">
|
<widget class="QStackedWidget" name="pluginStackedWidget">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -589,17 +651,102 @@ QLabel#coverLabel {
|
|||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="playlistViewPage">
|
||||||
|
<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 class="QWidget" name="chaptersViewPage">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<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="QTreeView" name="chapterlistView">
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<action name="actionHelp">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="system-help"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>About</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>About</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string notr="true">F1</string>
|
||||||
|
</property>
|
||||||
|
<property name="menuRole">
|
||||||
|
<enum>QAction::AboutRole</enum>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionOpen">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="document-open"/>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Open</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string notr="true">Ctrl+O</string>
|
||||||
|
</property>
|
||||||
|
<property name="menuRole">
|
||||||
|
<enum>QAction::NoRole</enum>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>SeekableSlider</class>
|
<class>SeekableSlider</class>
|
||||||
<extends>QSlider</extends>
|
<extends>QSlider</extends>
|
||||||
<header>seekableslider.h</header>
|
<header>seekableslider.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>PlaybackProgressIndicator</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>playbackprogressindicator.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="resources.qrc"/>
|
<include location="resources.qrc"/>
|
||||||
|
362
playbackprogressindicator.cpp
Normal file
362
playbackprogressindicator.cpp
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "playbackprogressindicator.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
extern "C" {
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavutil/avutil.h>
|
||||||
|
#include <libavutil/dict.h>
|
||||||
|
#include <libavutil/time.h> // Contains AV_TIME_BASE and AV_TIME_BASE_Q
|
||||||
|
} // extern "C"
|
||||||
|
#endif // HAVE_FFMPEG
|
||||||
|
|
||||||
|
PlaybackProgressIndicator::PlaybackProgressIndicator(QWidget *parent) :
|
||||||
|
QWidget(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaybackProgressIndicator::currentChapterItem() const
|
||||||
|
{
|
||||||
|
int currentChapterIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_chapterModel.rowCount(); i++) {
|
||||||
|
QStandardItem* timeItem = m_chapterModel.item(i, 0);
|
||||||
|
qint64 chapterStartTime = timeItem->data(PlaybackProgressIndicator::StartTimeMsRole).toLongLong();
|
||||||
|
|
||||||
|
if (m_position >= chapterStartTime) {
|
||||||
|
currentChapterIndex = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChapterIndex >= 0) {
|
||||||
|
return m_chapterModel.index(currentChapterIndex, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaybackProgressIndicator::currentChapterName() const
|
||||||
|
{
|
||||||
|
const QModelIndex timeIndex(currentChapterItem());
|
||||||
|
if (timeIndex.isValid()) {
|
||||||
|
return m_chapterModel.item(timeIndex.row(), 1)->text();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::setPosition(qint64 pos)
|
||||||
|
{
|
||||||
|
m_position = pos;
|
||||||
|
emit positionChanged(m_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::setDuration(qint64 dur)
|
||||||
|
{
|
||||||
|
m_duration = dur;
|
||||||
|
emit durationChanged(m_duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaybackProgressIndicator::formatTime(qint64 milliseconds)
|
||||||
|
{
|
||||||
|
QTime duaTime(QTime::fromMSecsSinceStartOfDay(milliseconds));
|
||||||
|
if (duaTime.hour() > 0) {
|
||||||
|
return duaTime.toString("h:mm:ss");
|
||||||
|
} else {
|
||||||
|
return duaTime.toString("m:ss");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::setChapters(QList<std::pair<qint64, QString> > chapters)
|
||||||
|
{
|
||||||
|
m_chapterModel.clear();
|
||||||
|
|
||||||
|
m_chapterModel.setHorizontalHeaderLabels(QStringList() << tr("Time") << tr("Chapter Name"));
|
||||||
|
|
||||||
|
for (const std::pair<qint64, QString> & chapter : chapters) {
|
||||||
|
QList<QStandardItem*> row;
|
||||||
|
|
||||||
|
QStandardItem * timeItem = new QStandardItem(formatTime(chapter.first));
|
||||||
|
timeItem->setData(chapter.first, StartTimeMsRole);
|
||||||
|
row.append(timeItem);
|
||||||
|
|
||||||
|
QStandardItem * chapterItem = new QStandardItem(chapter.second);
|
||||||
|
chapterItem->setData(chapter.first, StartTimeMsRole);
|
||||||
|
row.append(chapterItem);
|
||||||
|
|
||||||
|
m_chapterModel.appendRow(row);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::tryLoadChapters(const QString &filePath)
|
||||||
|
{
|
||||||
|
auto chapters = tryLoadSidecarChapterFile(filePath);
|
||||||
|
if (chapters.size() == 0) {
|
||||||
|
chapters = tryLoadChaptersFromMetadata(filePath);
|
||||||
|
}
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::tryLoadSidecarChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (filePath.endsWith(".chp", Qt::CaseInsensitive)) {
|
||||||
|
return parseCHPChapterFile(filePath);
|
||||||
|
} else if (filePath.endsWith(".pbf", Qt::CaseInsensitive)) {
|
||||||
|
return parsePBFChapterFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".chp"));
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parseCHPChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".pbf"));
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parsePBFChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
fileInfo.setFile(filePath + ".chp");
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parseCHPChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
|
||||||
|
// Helper function to convert FFmpeg time (in time_base units) to milliseconds
|
||||||
|
qint64 convertTimestampToMilliseconds(int64_t timestamp, AVRational time_base) {
|
||||||
|
// Convert to seconds first, then to milliseconds and cast to qint64
|
||||||
|
return static_cast<qint64>((double)timestamp * av_q2d(time_base) * 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to print FFmpeg errors
|
||||||
|
void printFFmpegError(int errnum) {
|
||||||
|
char errbuf[AV_ERROR_MAX_STRING_SIZE];
|
||||||
|
av_strerror(errnum, errbuf, sizeof(errbuf));
|
||||||
|
qCritical() << "FFmpeg error:" << errbuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // HAVE_FFMPEG
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::tryLoadChaptersFromMetadata(const QString &filePath)
|
||||||
|
{
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
if (!QFile::exists(filePath)) {
|
||||||
|
qCritical() << "Error: File not found" << filePath;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFormatContext* format_ctx = nullptr; // FFmpeg format context
|
||||||
|
int ret = 0; // Return value for FFmpeg functions
|
||||||
|
|
||||||
|
qInfo() << "Attempting to open file:" << filePath;
|
||||||
|
|
||||||
|
// Open the input file and read the header.
|
||||||
|
// The last two arguments (AVInputFormat*, AVDictionary**) are optional.
|
||||||
|
// Passing nullptr for them means FFmpeg will try to guess the format
|
||||||
|
// and no options will be passed to the demuxer.
|
||||||
|
ret = avformat_open_input(&format_ctx, filePath.toUtf8().constData(), nullptr, nullptr);
|
||||||
|
if (ret < 0) {
|
||||||
|
qCritical() << "Could not open input file:" << filePath;
|
||||||
|
printFFmpegError(ret);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
qInfo() << "File opened successfully.";
|
||||||
|
|
||||||
|
// Read stream information from the file.
|
||||||
|
// This populates format_ctx->streams and other metadata, including chapters.
|
||||||
|
ret = avformat_find_stream_info(format_ctx, nullptr);
|
||||||
|
if (ret < 0) {
|
||||||
|
qCritical() << "Could not find stream information for file:" << filePath;
|
||||||
|
printFFmpegError(ret);
|
||||||
|
avformat_close_input(&format_ctx); // Close the context before returning
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
qInfo() << "Stream information found.";
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString>> chapterList;
|
||||||
|
|
||||||
|
// Check if there are any chapters
|
||||||
|
if (format_ctx->nb_chapters == 0) {
|
||||||
|
qInfo() << "No chapters found in file:" << filePath;
|
||||||
|
} else {
|
||||||
|
qInfo() << "Found" << format_ctx->nb_chapters << "chapters.";
|
||||||
|
// Iterate through each chapter
|
||||||
|
for (unsigned int i = 0; i < format_ctx->nb_chapters; ++i) {
|
||||||
|
AVChapter* chapter = format_ctx->chapters[i];
|
||||||
|
|
||||||
|
// Chapter timestamps are typically in AV_TIME_BASE units by default
|
||||||
|
// unless the chapter itself has a specific time_base.
|
||||||
|
// For simplicity and common cases, we use AV_TIME_BASE_Q.
|
||||||
|
qint64 start_ms = convertTimestampToMilliseconds(chapter->start, chapter->time_base);
|
||||||
|
|
||||||
|
// Get the chapter title from its metadata.
|
||||||
|
// av_dict_get(dictionary, key, prev, flags)
|
||||||
|
// prev is used for iterating through multiple entries with the same key,
|
||||||
|
// we want the first one so we pass nullptr. flags=0 for case-insensitive.
|
||||||
|
AVDictionaryEntry* title_tag = av_dict_get(chapter->metadata, "title", nullptr, 0);
|
||||||
|
QString chapter_title = (title_tag && title_tag->value) ? QString::fromUtf8(title_tag->value) : "Untitled Chapter";
|
||||||
|
|
||||||
|
chapterList.append(std::make_pair(start_ms, chapter_title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the input file.
|
||||||
|
// This also frees the format_ctx and associated data.
|
||||||
|
avformat_close_input(&format_ctx);
|
||||||
|
qInfo() << "File closed.";
|
||||||
|
|
||||||
|
return chapterList;
|
||||||
|
#else
|
||||||
|
qInfo() << "FFmpeg not found during build.";
|
||||||
|
return {};
|
||||||
|
#endif // HAVE_FFMPEG
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::parseCHPChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QList<std::pair<qint64, QString>> chapters;
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
QRegularExpression timeRegex(R"((\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)");
|
||||||
|
|
||||||
|
while (!in.atEnd()) {
|
||||||
|
QString line = in.readLine().trimmed();
|
||||||
|
QRegularExpressionMatch match = timeRegex.match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
int hours = match.capturedView(3).isEmpty() ? 0 : match.capturedView(1).toInt();
|
||||||
|
int minutes = match.capturedView(3).isEmpty() ? match.capturedView(1).toInt() : match.capturedView(2).toInt();
|
||||||
|
int seconds = match.capturedView(3).isEmpty() ? match.capturedView(2).toInt() : match.capturedView(3).toInt();
|
||||||
|
int milliseconds = 0;
|
||||||
|
|
||||||
|
QStringView millisecondsStr(match.capturedView(4));
|
||||||
|
if (!millisecondsStr.isEmpty()) {
|
||||||
|
milliseconds = millisecondsStr.toInt() * pow(10, 3 - millisecondsStr.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 totalMilliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds;
|
||||||
|
|
||||||
|
QString chapterTitle = line.mid(match.capturedLength()).trimmed();
|
||||||
|
chapters.append(std::make_pair(totalMilliseconds, chapterTitle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::parsePBFChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QList<std::pair<qint64, QString>> chapters;
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
QRegularExpression chapterRegex(R"(^\d+=(\d+)\*([^*]*)\*.*$)");
|
||||||
|
|
||||||
|
while (!in.atEnd()) {
|
||||||
|
QString line = in.readLine().trimmed();
|
||||||
|
QRegularExpressionMatch match = chapterRegex.match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
qint64 timestamp = match.captured(1).toLongLong();
|
||||||
|
QString title = match.captured(2).trimmed();
|
||||||
|
chapters.append(std::make_pair(timestamp, title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::paintEvent(QPaintEvent *event)
|
||||||
|
{
|
||||||
|
constexpr int progressBarHeight = 6;
|
||||||
|
constexpr QColor activeColor = QColor(85, 170, 0);
|
||||||
|
const QPointF topLeft(0, height() / 2.0 - progressBarHeight / 2.0);
|
||||||
|
const QSizeF barSize(width(), progressBarHeight);
|
||||||
|
|
||||||
|
const float currentProgress = m_duration <= 0 ? 0 : (m_seekingPosition >= 0 ? m_seekingPosition : m_position) / (float)m_duration;
|
||||||
|
const QSizeF progressSize(width() * currentProgress, progressBarHeight);
|
||||||
|
|
||||||
|
QPainterPath theProgress;
|
||||||
|
theProgress.addRoundedRect(QRectF(topLeft, progressSize), progressBarHeight / 2, progressBarHeight / 2);
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.save();
|
||||||
|
|
||||||
|
// the bar itself
|
||||||
|
painter.setPen(Qt::gray);
|
||||||
|
painter.drawRoundedRect(QRectF(topLeft, barSize), progressBarHeight / 2, progressBarHeight / 2);
|
||||||
|
painter.fillPath(theProgress, activeColor);
|
||||||
|
|
||||||
|
// progress
|
||||||
|
painter.setPen(activeColor);
|
||||||
|
painter.drawPath(theProgress);
|
||||||
|
|
||||||
|
// chapter markers
|
||||||
|
if (m_duration > 0) {
|
||||||
|
painter.setPen(Qt::lightGray);
|
||||||
|
for (int i = 0; i < m_chapterModel.rowCount(); i++) {
|
||||||
|
qint64 chapterStartTime = m_chapterModel.item(i)->data(StartTimeMsRole).toInt();
|
||||||
|
if (chapterStartTime == 0) continue;
|
||||||
|
if (chapterStartTime > m_duration) break;
|
||||||
|
float chapterPercent = chapterStartTime / (float)m_duration;
|
||||||
|
float chapterPosX = width() * chapterPercent;
|
||||||
|
painter.drawLine(topLeft + QPoint(chapterPosX, 0),
|
||||||
|
topLeft + QPoint(chapterPosX, progressBarHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (m_duration > 0) {
|
||||||
|
event->accept();
|
||||||
|
} else {
|
||||||
|
return QWidget::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::mouseMoveEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (m_duration > 0) {
|
||||||
|
m_seekingPosition = event->position().x() * m_duration / width();
|
||||||
|
if (m_seekOnMove) {
|
||||||
|
emit seekingRequested(m_seekingPosition);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
return QWidget::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackProgressIndicator::mouseReleaseEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (m_duration > 0) {
|
||||||
|
int seekingPosition = event->position().x() * m_duration / width();
|
||||||
|
m_seekingPosition = -1;
|
||||||
|
emit seekingRequested(seekingPosition);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
return QWidget::mouseReleaseEvent(event);
|
||||||
|
}
|
63
playbackprogressindicator.h
Normal file
63
playbackprogressindicator.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
|
||||||
|
class PlaybackProgressIndicator : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(bool seekOnMove MEMBER m_seekOnMove NOTIFY seekOnMoveChanged)
|
||||||
|
Q_PROPERTY(qint64 position MEMBER m_position NOTIFY positionChanged)
|
||||||
|
Q_PROPERTY(qint64 duration MEMBER m_duration NOTIFY durationChanged)
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
ChapterTitleRole = Qt::DisplayRole,
|
||||||
|
StartTimeMsRole = Qt::UserRole + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit PlaybackProgressIndicator(QWidget *parent = nullptr);
|
||||||
|
~PlaybackProgressIndicator() = default;
|
||||||
|
|
||||||
|
QStandardItemModel* chapterModel() { return &m_chapterModel; }
|
||||||
|
QModelIndex currentChapterItem() const;
|
||||||
|
QString currentChapterName() const;
|
||||||
|
|
||||||
|
void setPosition(qint64 pos);
|
||||||
|
void setDuration(qint64 dur);
|
||||||
|
void setChapters(QList<std::pair<qint64, QString>> chapters);
|
||||||
|
|
||||||
|
static QString formatTime(qint64 milliseconds);
|
||||||
|
static QList<std::pair<qint64, QString>> tryLoadChapters(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> tryLoadSidecarChapterFile(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> tryLoadChaptersFromMetadata(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> parseCHPChapterFile(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> parsePBFChapterFile(const QString & filePath);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void seekOnMoveChanged(bool sow);
|
||||||
|
void positionChanged(qint64 newPosition);
|
||||||
|
void durationChanged(qint64 newDuration);
|
||||||
|
void seekingRequested(qint64 position);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_seekOnMove = true;
|
||||||
|
qint64 m_position = -1;
|
||||||
|
qint64 m_seekingPosition = -1;
|
||||||
|
qint64 m_duration = -1;
|
||||||
|
QStandardItemModel m_chapterModel;
|
||||||
|
};
|
||||||
|
|
283
playlistmanager.cpp
Normal file
283
playlistmanager.cpp
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 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()
|
||||||
|
= default;
|
||||||
|
|
||||||
|
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_playlist = urls;
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
|
||||||
|
{
|
||||||
|
if (urls.isEmpty()) return {};
|
||||||
|
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 {};
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
return m_playlist.at(index.row()).fileName();
|
||||||
|
case UrlRole:
|
||||||
|
return m_playlist.at(index.row());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||||
|
{
|
||||||
|
QFile file(url.toLocalFile());
|
||||||
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
QList<QUrl> urls;
|
||||||
|
while (!file.atEnd()) {
|
||||||
|
QString line = file.readLine();
|
||||||
|
if (line.startsWith('#')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QFileInfo fileInfo(file);
|
||||||
|
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
|
||||||
|
urls.append(item);
|
||||||
|
}
|
||||||
|
return loadPlaylist(urls);
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int PlaylistManager::totalCount() const
|
||||||
|
{
|
||||||
|
return m_model.rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistManager::previousIndex() const
|
||||||
|
{
|
||||||
|
int count = totalCount();
|
||||||
|
if (count == 0) return {};
|
||||||
|
|
||||||
|
return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistManager::nextIndex() const
|
||||||
|
{
|
||||||
|
int count = totalCount();
|
||||||
|
if (count == 0) return {};
|
||||||
|
|
||||||
|
return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistManager::curIndex() const
|
||||||
|
{
|
||||||
|
return m_model.index(m_currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::isFirstIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::isLastIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex + 1 == totalCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
88
playlistmanager.h
Normal file
88
playlistmanager.h
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 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() override;
|
||||||
|
|
||||||
|
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);
|
||||||
|
Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url);
|
||||||
|
|
||||||
|
int totalCount() const;
|
||||||
|
QModelIndex previousIndex() const;
|
||||||
|
QModelIndex nextIndex() const;
|
||||||
|
QModelIndex curIndex() const;
|
||||||
|
bool isFirstIndex() const;
|
||||||
|
bool isLastIndex() 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;
|
||||||
|
};
|
@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2017 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
|
||||||
|
|
||||||
#include "playlistmodel.h"
|
|
||||||
#include "qt/qmediaplaylist.h"
|
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
PlaylistModel::PlaylistModel(QObject *parent)
|
|
||||||
: QAbstractItemModel(parent)
|
|
||||||
{
|
|
||||||
m_playlist.reset(new QMediaPlaylist);
|
|
||||||
connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeInserted, this, &PlaylistModel::beginInsertItems);
|
|
||||||
connect(m_playlist.data(), &QMediaPlaylist::mediaInserted, this, &PlaylistModel::endInsertItems);
|
|
||||||
connect(m_playlist.data(), &QMediaPlaylist::mediaAboutToBeRemoved, this, &PlaylistModel::beginRemoveItems);
|
|
||||||
connect(m_playlist.data(), &QMediaPlaylist::mediaRemoved, this, &PlaylistModel::endRemoveItems);
|
|
||||||
connect(m_playlist.data(), &QMediaPlaylist::mediaChanged, this, &PlaylistModel::changeItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistModel::~PlaylistModel() = default;
|
|
||||||
|
|
||||||
int PlaylistModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_playlist && !parent.isValid() ? m_playlist->mediaCount() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PlaylistModel::columnCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return !parent.isValid() ? ColumnCount : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex PlaylistModel::index(int row, int column, const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_playlist && !parent.isValid()
|
|
||||||
&& row >= 0 && row < m_playlist->mediaCount()
|
|
||||||
&& column >= 0 && column < ColumnCount
|
|
||||||
? createIndex(row, column)
|
|
||||||
: QModelIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex PlaylistModel::parent(const QModelIndex &child) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(child);
|
|
||||||
|
|
||||||
return QModelIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (index.isValid() && role == Qt::DisplayRole) {
|
|
||||||
QVariant value = m_data[index];
|
|
||||||
if (!value.isValid() && index.column() == Title) {
|
|
||||||
QUrl location = m_playlist->media(index.row());
|
|
||||||
return QFileInfo(location.path()).fileName();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
QMediaPlaylist *PlaylistModel::playlist() const
|
|
||||||
{
|
|
||||||
return m_playlist.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PlaylistModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
||||||
{
|
|
||||||
Q_UNUSED(role);
|
|
||||||
m_data[index] = value;
|
|
||||||
emit dataChanged(index, index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaylistModel::beginInsertItems(int start, int end)
|
|
||||||
{
|
|
||||||
m_data.clear();
|
|
||||||
beginInsertRows(QModelIndex(), start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaylistModel::endInsertItems()
|
|
||||||
{
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaylistModel::beginRemoveItems(int start, int end)
|
|
||||||
{
|
|
||||||
m_data.clear();
|
|
||||||
beginRemoveRows(QModelIndex(), start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaylistModel::endRemoveItems()
|
|
||||||
{
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaylistModel::changeItems(int start, int end)
|
|
||||||
{
|
|
||||||
m_data.clear();
|
|
||||||
emit dataChanged(index(start,0), index(end,ColumnCount));
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
// Copyright (C) 2017 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
|
||||||
|
|
||||||
#ifndef PLAYLISTMODEL_H
|
|
||||||
#define PLAYLISTMODEL_H
|
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
|
||||||
#include <QScopedPointer>
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QMediaPlaylist;
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
class PlaylistModel : public QAbstractItemModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Column
|
|
||||||
{
|
|
||||||
Title = 0,
|
|
||||||
ColumnCount
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit PlaylistModel(QObject *parent = nullptr);
|
|
||||||
~PlaylistModel();
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
|
|
||||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QModelIndex parent(const QModelIndex &child) const override;
|
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
|
|
||||||
QMediaPlaylist *playlist() const;
|
|
||||||
|
|
||||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void beginInsertItems(int start, int end);
|
|
||||||
void endInsertItems();
|
|
||||||
void beginRemoveItems(int start, int end);
|
|
||||||
void endRemoveItems();
|
|
||||||
void changeItems(int start, int end);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QScopedPointer<QMediaPlaylist> m_playlist;
|
|
||||||
QMap<QModelIndex, QVariant> m_data;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // PLAYLISTMODEL_H
|
|
@ -1,653 +0,0 @@
|
|||||||
// Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
|
|
||||||
#include "qmediaplaylist.h"
|
|
||||||
#include "qmediaplaylist_p.h"
|
|
||||||
#include "qplaylistfileparser_p.h"
|
|
||||||
|
|
||||||
#include <QtCore/qlist.h>
|
|
||||||
#include <QtCore/qfile.h>
|
|
||||||
#include <QtCore/qurl.h>
|
|
||||||
#include <QtCore/qcoreevent.h>
|
|
||||||
#include <QtCore/qcoreapplication.h>
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
|
|
||||||
class QM3uPlaylistWriter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
QM3uPlaylistWriter(QIODevice *device)
|
|
||||||
:m_device(device), m_textStream(new QTextStream(m_device))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~QM3uPlaylistWriter()
|
|
||||||
{
|
|
||||||
delete m_textStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool writeItem(const QUrl& item)
|
|
||||||
{
|
|
||||||
*m_textStream << item.toString() << Qt::endl;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QIODevice *m_device;
|
|
||||||
QTextStream *m_textStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
int QMediaPlaylistPrivate::nextPosition(int steps) const
|
|
||||||
{
|
|
||||||
if (playlist.count() == 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
int next = currentPos + steps;
|
|
||||||
|
|
||||||
switch (playbackMode) {
|
|
||||||
case QMediaPlaylist::CurrentItemOnce:
|
|
||||||
return steps != 0 ? -1 : currentPos;
|
|
||||||
case QMediaPlaylist::CurrentItemInLoop:
|
|
||||||
return currentPos;
|
|
||||||
case QMediaPlaylist::Sequential:
|
|
||||||
if (next >= playlist.size())
|
|
||||||
next = -1;
|
|
||||||
break;
|
|
||||||
case QMediaPlaylist::Loop:
|
|
||||||
next %= playlist.count();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
int QMediaPlaylistPrivate::prevPosition(int steps) const
|
|
||||||
{
|
|
||||||
if (playlist.count() == 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
int next = currentPos;
|
|
||||||
if (next < 0)
|
|
||||||
next = playlist.size();
|
|
||||||
next -= steps;
|
|
||||||
|
|
||||||
switch (playbackMode) {
|
|
||||||
case QMediaPlaylist::CurrentItemOnce:
|
|
||||||
return steps != 0 ? -1 : currentPos;
|
|
||||||
case QMediaPlaylist::CurrentItemInLoop:
|
|
||||||
return currentPos;
|
|
||||||
case QMediaPlaylist::Sequential:
|
|
||||||
if (next < 0)
|
|
||||||
next = -1;
|
|
||||||
break;
|
|
||||||
case QMediaPlaylist::Loop:
|
|
||||||
next %= playlist.size();
|
|
||||||
if (next < 0)
|
|
||||||
next += playlist.size();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\class QMediaPlaylist
|
|
||||||
\inmodule QtMultimedia
|
|
||||||
\ingroup multimedia
|
|
||||||
\ingroup multimedia_playback
|
|
||||||
|
|
||||||
|
|
||||||
\brief The QMediaPlaylist class provides a list of media content to play.
|
|
||||||
|
|
||||||
QMediaPlaylist is intended to be used with other media objects,
|
|
||||||
like QMediaPlayer.
|
|
||||||
|
|
||||||
QMediaPlaylist allows to access the service intrinsic playlist functionality
|
|
||||||
if available, otherwise it provides the local memory playlist implementation.
|
|
||||||
|
|
||||||
\snippet multimedia-snippets/media.cpp Movie playlist
|
|
||||||
|
|
||||||
Depending on playlist source implementation, most of the playlist mutating
|
|
||||||
operations can be asynchronous.
|
|
||||||
|
|
||||||
QMediaPlayList currently supports M3U playlists (file extension .m3u and .m3u8).
|
|
||||||
|
|
||||||
\sa QUrl
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\enum QMediaPlaylist::PlaybackMode
|
|
||||||
|
|
||||||
The QMediaPlaylist::PlaybackMode describes the order items in playlist are played.
|
|
||||||
|
|
||||||
\value CurrentItemOnce The current item is played only once.
|
|
||||||
|
|
||||||
\value CurrentItemInLoop The current item is played repeatedly in a loop.
|
|
||||||
|
|
||||||
\value Sequential Playback starts from the current and moves through each successive item until the last is reached and then stops.
|
|
||||||
The next item is a null item when the last one is currently playing.
|
|
||||||
|
|
||||||
\value Loop Playback restarts at the first item after the last has finished playing.
|
|
||||||
|
|
||||||
\value Random Play items in random order.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Create a new playlist object with the given \a parent.
|
|
||||||
*/
|
|
||||||
|
|
||||||
QMediaPlaylist::QMediaPlaylist(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, d_ptr(new QMediaPlaylistPrivate)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
|
|
||||||
d->q_ptr = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Destroys the playlist.
|
|
||||||
*/
|
|
||||||
|
|
||||||
QMediaPlaylist::~QMediaPlaylist()
|
|
||||||
{
|
|
||||||
delete d_ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\property QMediaPlaylist::playbackMode
|
|
||||||
|
|
||||||
This property defines the order that items in the playlist are played.
|
|
||||||
|
|
||||||
\sa QMediaPlaylist::PlaybackMode
|
|
||||||
*/
|
|
||||||
|
|
||||||
QMediaPlaylist::PlaybackMode QMediaPlaylist::playbackMode() const
|
|
||||||
{
|
|
||||||
return d_func()->playbackMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QMediaPlaylist::setPlaybackMode(QMediaPlaylist::PlaybackMode mode)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
|
|
||||||
if (mode == d->playbackMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
d->playbackMode = mode;
|
|
||||||
|
|
||||||
emit playbackModeChanged(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns position of the current media content in the playlist.
|
|
||||||
*/
|
|
||||||
int QMediaPlaylist::currentIndex() const
|
|
||||||
{
|
|
||||||
return d_func()->currentPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the current media content.
|
|
||||||
*/
|
|
||||||
|
|
||||||
QUrl QMediaPlaylist::currentMedia() const
|
|
||||||
{
|
|
||||||
Q_D(const QMediaPlaylist);
|
|
||||||
if (d->currentPos < 0 || d->currentPos >= d->playlist.size())
|
|
||||||
return QUrl();
|
|
||||||
return d_func()->playlist.at(d_func()->currentPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the index of the item, which would be current after calling next()
|
|
||||||
\a steps times.
|
|
||||||
|
|
||||||
Returned value depends on the size of playlist, current position
|
|
||||||
and playback mode.
|
|
||||||
|
|
||||||
\sa QMediaPlaylist::playbackMode(), previousIndex()
|
|
||||||
*/
|
|
||||||
int QMediaPlaylist::nextIndex(int steps) const
|
|
||||||
{
|
|
||||||
return d_func()->nextPosition(steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the index of the item, which would be current after calling previous()
|
|
||||||
\a steps times.
|
|
||||||
|
|
||||||
\sa QMediaPlaylist::playbackMode(), nextIndex()
|
|
||||||
*/
|
|
||||||
|
|
||||||
int QMediaPlaylist::previousIndex(int steps) const
|
|
||||||
{
|
|
||||||
return d_func()->prevPosition(steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the number of items in the playlist.
|
|
||||||
|
|
||||||
\sa isEmpty()
|
|
||||||
*/
|
|
||||||
int QMediaPlaylist::mediaCount() const
|
|
||||||
{
|
|
||||||
return d_func()->playlist.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns true if the playlist contains no items, otherwise returns false.
|
|
||||||
|
|
||||||
\sa mediaCount()
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::isEmpty() const
|
|
||||||
{
|
|
||||||
return mediaCount() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the media content at \a index in the playlist.
|
|
||||||
*/
|
|
||||||
|
|
||||||
QUrl QMediaPlaylist::media(int index) const
|
|
||||||
{
|
|
||||||
Q_D(const QMediaPlaylist);
|
|
||||||
if (index < 0 || index >= d->playlist.size())
|
|
||||||
return QUrl();
|
|
||||||
return d->playlist.at(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Append the media \a content to the playlist.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise returns false.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::addMedia(const QUrl &content)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
int pos = d->playlist.size();
|
|
||||||
emit mediaAboutToBeInserted(pos, pos);
|
|
||||||
d->playlist.append(content);
|
|
||||||
emit mediaInserted(pos, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Append multiple media content \a items to the playlist.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise returns false.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::addMedia(const QList<QUrl> &items)
|
|
||||||
{
|
|
||||||
if (!items.size())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
int first = d->playlist.size();
|
|
||||||
int last = first + items.size() - 1;
|
|
||||||
emit mediaAboutToBeInserted(first, last);
|
|
||||||
d_func()->playlist.append(items);
|
|
||||||
emit mediaInserted(first, last);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Insert the media \a content to the playlist at position \a pos.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise returns false.
|
|
||||||
*/
|
|
||||||
|
|
||||||
bool QMediaPlaylist::insertMedia(int pos, const QUrl &content)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
pos = qBound(0, pos, d->playlist.size());
|
|
||||||
emit mediaAboutToBeInserted(pos, pos);
|
|
||||||
d->playlist.insert(pos, content);
|
|
||||||
emit mediaInserted(pos, pos);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Insert multiple media content \a items to the playlist at position \a pos.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise returns false.
|
|
||||||
*/
|
|
||||||
|
|
||||||
bool QMediaPlaylist::insertMedia(int pos, const QList<QUrl> &items)
|
|
||||||
{
|
|
||||||
if (!items.size())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
pos = qBound(0, pos, d->playlist.size());
|
|
||||||
int last = pos + items.size() - 1;
|
|
||||||
emit mediaAboutToBeInserted(pos, last);
|
|
||||||
auto newList = d->playlist.mid(0, pos);
|
|
||||||
newList += items;
|
|
||||||
newList += d->playlist.mid(pos);
|
|
||||||
d->playlist = newList;
|
|
||||||
emit mediaInserted(pos, last);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Move the item from position \a from to position \a to.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise false.
|
|
||||||
|
|
||||||
\since 5.7
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::moveMedia(int from, int to)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
if (from < 0 || from > d->playlist.count() ||
|
|
||||||
to < 0 || to > d->playlist.count())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
d->playlist.move(from, to);
|
|
||||||
emit mediaChanged(from, to);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Remove the item from the playlist at position \a pos.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise return false.
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::removeMedia(int pos)
|
|
||||||
{
|
|
||||||
return removeMedia(pos, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Remove items in the playlist from \a start to \a end inclusive.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise return false.
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::removeMedia(int start, int end)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
if (end < start || end < 0 || start >= d->playlist.count())
|
|
||||||
return false;
|
|
||||||
start = qBound(0, start, d->playlist.size() - 1);
|
|
||||||
end = qBound(0, end, d->playlist.size() - 1);
|
|
||||||
|
|
||||||
emit mediaAboutToBeRemoved(start, end);
|
|
||||||
d->playlist.remove(start, end - start + 1);
|
|
||||||
emit mediaRemoved(start, end);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Remove all the items from the playlist.
|
|
||||||
|
|
||||||
Returns true if the operation is successful, otherwise return false.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::clear()
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
int size = d->playlist.size();
|
|
||||||
emit mediaAboutToBeRemoved(0, size - 1);
|
|
||||||
d->playlist.clear();
|
|
||||||
emit mediaRemoved(0, size - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Load playlist from \a location. If \a format is specified, it is used,
|
|
||||||
otherwise format is guessed from location name and data.
|
|
||||||
|
|
||||||
New items are appended to playlist.
|
|
||||||
|
|
||||||
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
|
|
||||||
otherwise the playlist emits loadFailed().
|
|
||||||
*/
|
|
||||||
|
|
||||||
void QMediaPlaylist::load(const QUrl &location, const char *format)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
|
|
||||||
d->error = NoError;
|
|
||||||
d->errorString.clear();
|
|
||||||
|
|
||||||
d->ensureParser();
|
|
||||||
d->parser->start(location, QString::fromUtf8(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Load playlist from QIODevice \a device. If \a format is specified, it is used,
|
|
||||||
otherwise format is guessed from device data.
|
|
||||||
|
|
||||||
New items are appended to playlist.
|
|
||||||
|
|
||||||
QMediaPlaylist::loaded() signal is emitted if playlist was loaded successfully,
|
|
||||||
otherwise the playlist emits loadFailed().
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::load(QIODevice *device, const char *format)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
|
|
||||||
d->error = NoError;
|
|
||||||
d->errorString.clear();
|
|
||||||
|
|
||||||
d->ensureParser();
|
|
||||||
d->parser->start(device, QString::fromUtf8(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Save playlist to \a location. If \a format is specified, it is used,
|
|
||||||
otherwise format is guessed from location name.
|
|
||||||
|
|
||||||
Returns true if playlist was saved successfully, otherwise returns false.
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::save(const QUrl &location, const char *format) const
|
|
||||||
{
|
|
||||||
Q_D(const QMediaPlaylist);
|
|
||||||
|
|
||||||
d->error = NoError;
|
|
||||||
d->errorString.clear();
|
|
||||||
|
|
||||||
if (!d->checkFormat(format))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QFile file(location.toLocalFile());
|
|
||||||
|
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
||||||
d->error = AccessDeniedError;
|
|
||||||
d->errorString = tr("The file could not be accessed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return save(&file, format);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Save playlist to QIODevice \a device using format \a format.
|
|
||||||
|
|
||||||
Returns true if playlist was saved successfully, otherwise returns false.
|
|
||||||
*/
|
|
||||||
bool QMediaPlaylist::save(QIODevice *device, const char *format) const
|
|
||||||
{
|
|
||||||
Q_D(const QMediaPlaylist);
|
|
||||||
|
|
||||||
d->error = NoError;
|
|
||||||
d->errorString.clear();
|
|
||||||
|
|
||||||
if (!d->checkFormat(format))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QM3uPlaylistWriter writer(device);
|
|
||||||
for (const auto &entry : d->playlist)
|
|
||||||
writer.writeItem(entry);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the last error condition.
|
|
||||||
*/
|
|
||||||
QMediaPlaylist::Error QMediaPlaylist::error() const
|
|
||||||
{
|
|
||||||
return d_func()->error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Returns the string describing the last error condition.
|
|
||||||
*/
|
|
||||||
QString QMediaPlaylist::errorString() const
|
|
||||||
{
|
|
||||||
return d_func()->errorString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Shuffle items in the playlist.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::shuffle()
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
QList<QUrl> playlist;
|
|
||||||
|
|
||||||
// keep the current item when shuffling
|
|
||||||
QUrl current;
|
|
||||||
if (d->currentPos != -1)
|
|
||||||
current = d->playlist.takeAt(d->currentPos);
|
|
||||||
|
|
||||||
while (!d->playlist.isEmpty())
|
|
||||||
playlist.append(d->playlist.takeAt(QRandomGenerator::global()->bounded(int(d->playlist.size()))));
|
|
||||||
|
|
||||||
if (d->currentPos != -1)
|
|
||||||
playlist.insert(d->currentPos, current);
|
|
||||||
d->playlist = playlist;
|
|
||||||
emit mediaChanged(0, d->playlist.count());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Advance to the next media content in playlist.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::next()
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
d->currentPos = d->nextPosition(1);
|
|
||||||
|
|
||||||
emit currentIndexChanged(d->currentPos);
|
|
||||||
emit currentMediaChanged(currentMedia());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Return to the previous media content in playlist.
|
|
||||||
*/
|
|
||||||
void QMediaPlaylist::previous()
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
d->currentPos = d->prevPosition(1);
|
|
||||||
|
|
||||||
emit currentIndexChanged(d->currentPos);
|
|
||||||
emit currentMediaChanged(currentMedia());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Activate media content from playlist at position \a playlistPosition.
|
|
||||||
*/
|
|
||||||
|
|
||||||
void QMediaPlaylist::setCurrentIndex(int playlistPosition)
|
|
||||||
{
|
|
||||||
Q_D(QMediaPlaylist);
|
|
||||||
if (playlistPosition < 0 || playlistPosition >= d->playlist.size())
|
|
||||||
playlistPosition = -1;
|
|
||||||
d->currentPos = playlistPosition;
|
|
||||||
|
|
||||||
emit currentIndexChanged(d->currentPos);
|
|
||||||
emit currentMediaChanged(currentMedia());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::mediaInserted(int start, int end)
|
|
||||||
|
|
||||||
This signal is emitted after media has been inserted into the playlist.
|
|
||||||
The new items are those between \a start and \a end inclusive.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::mediaRemoved(int start, int end)
|
|
||||||
|
|
||||||
This signal is emitted after media has been removed from the playlist.
|
|
||||||
The removed items are those between \a start and \a end inclusive.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::mediaChanged(int start, int end)
|
|
||||||
|
|
||||||
This signal is emitted after media has been changed in the playlist
|
|
||||||
between \a start and \a end positions inclusive.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::currentIndexChanged(int position)
|
|
||||||
|
|
||||||
Signal emitted when playlist position changed to \a position.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::playbackModeChanged(QMediaPlaylist::PlaybackMode mode)
|
|
||||||
|
|
||||||
Signal emitted when playback mode changed to \a mode.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::mediaAboutToBeInserted(int start, int end)
|
|
||||||
|
|
||||||
Signal emitted when items are to be inserted at \a start and ending at \a end.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::mediaAboutToBeRemoved(int start, int end)
|
|
||||||
|
|
||||||
Signal emitted when item are to be deleted at \a start and ending at \a end.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn void QMediaPlaylist::currentMediaChanged(const QUrl &content)
|
|
||||||
|
|
||||||
Signal emitted when current media changes to \a content.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\property QMediaPlaylist::currentIndex
|
|
||||||
\brief Current position.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\property QMediaPlaylist::currentMedia
|
|
||||||
\brief Current media content.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn QMediaPlaylist::loaded()
|
|
||||||
|
|
||||||
Signal emitted when playlist finished loading.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\fn QMediaPlaylist::loadFailed()
|
|
||||||
|
|
||||||
Signal emitted if failed to load playlist.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
\enum QMediaPlaylist::Error
|
|
||||||
|
|
||||||
This enum describes the QMediaPlaylist error codes.
|
|
||||||
|
|
||||||
\value NoError No errors.
|
|
||||||
\value FormatError Format error.
|
|
||||||
\value FormatNotSupportedError Format not supported.
|
|
||||||
\value NetworkError Network error.
|
|
||||||
\value AccessDeniedError Access denied error.
|
|
||||||
*/
|
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
#include "moc_qmediaplaylist.cpp"
|
|
@ -1,96 +0,0 @@
|
|||||||
// Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
|
|
||||||
#ifndef QMEDIAPLAYLIST_H
|
|
||||||
#define QMEDIAPLAYLIST_H
|
|
||||||
|
|
||||||
#include <QtCore/qobject.h>
|
|
||||||
|
|
||||||
#include <QtMultimedia/qtmultimediaglobal.h>
|
|
||||||
#include <QtMultimedia/qmediaenumdebug.h>
|
|
||||||
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
|
|
||||||
class QMediaPlaylistPrivate;
|
|
||||||
class QMediaPlaylist : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
Q_PROPERTY(QMediaPlaylist::PlaybackMode playbackMode READ playbackMode WRITE setPlaybackMode NOTIFY playbackModeChanged)
|
|
||||||
Q_PROPERTY(QUrl currentMedia READ currentMedia NOTIFY currentMediaChanged)
|
|
||||||
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum PlaybackMode { CurrentItemOnce, CurrentItemInLoop, Sequential, Loop };
|
|
||||||
Q_ENUM(PlaybackMode)
|
|
||||||
enum Error { NoError, FormatError, FormatNotSupportedError, NetworkError, AccessDeniedError };
|
|
||||||
Q_ENUM(Error)
|
|
||||||
|
|
||||||
explicit QMediaPlaylist(QObject *parent = nullptr);
|
|
||||||
virtual ~QMediaPlaylist();
|
|
||||||
|
|
||||||
PlaybackMode playbackMode() const;
|
|
||||||
void setPlaybackMode(PlaybackMode mode);
|
|
||||||
|
|
||||||
int currentIndex() const;
|
|
||||||
QUrl currentMedia() const;
|
|
||||||
|
|
||||||
int nextIndex(int steps = 1) const;
|
|
||||||
int previousIndex(int steps = 1) const;
|
|
||||||
|
|
||||||
QUrl media(int index) const;
|
|
||||||
|
|
||||||
int mediaCount() const;
|
|
||||||
bool isEmpty() const;
|
|
||||||
|
|
||||||
void addMedia(const QUrl &content);
|
|
||||||
void addMedia(const QList<QUrl> &items);
|
|
||||||
bool insertMedia(int index, const QUrl &content);
|
|
||||||
bool insertMedia(int index, const QList<QUrl> &items);
|
|
||||||
bool moveMedia(int from, int to);
|
|
||||||
bool removeMedia(int pos);
|
|
||||||
bool removeMedia(int start, int end);
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
void load(const QUrl &location, const char *format = nullptr);
|
|
||||||
void load(QIODevice *device, const char *format = nullptr);
|
|
||||||
|
|
||||||
bool save(const QUrl &location, const char *format = nullptr) const;
|
|
||||||
bool save(QIODevice *device, const char *format) const;
|
|
||||||
|
|
||||||
Error error() const;
|
|
||||||
QString errorString() const;
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
|
||||||
void shuffle();
|
|
||||||
|
|
||||||
void next();
|
|
||||||
void previous();
|
|
||||||
|
|
||||||
void setCurrentIndex(int index);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void currentIndexChanged(int index);
|
|
||||||
void playbackModeChanged(QMediaPlaylist::PlaybackMode mode);
|
|
||||||
void currentMediaChanged(const QUrl&);
|
|
||||||
|
|
||||||
void mediaAboutToBeInserted(int start, int end);
|
|
||||||
void mediaInserted(int start, int end);
|
|
||||||
void mediaAboutToBeRemoved(int start, int end);
|
|
||||||
void mediaRemoved(int start, int end);
|
|
||||||
void mediaChanged(int start, int end);
|
|
||||||
|
|
||||||
void loaded();
|
|
||||||
void loadFailed();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QMediaPlaylistPrivate *d_ptr;
|
|
||||||
Q_DECLARE_PRIVATE(QMediaPlaylist)
|
|
||||||
};
|
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode)
|
|
||||||
Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error)
|
|
||||||
|
|
||||||
#endif // QMEDIAPLAYLIST_H
|
|
@ -1,112 +0,0 @@
|
|||||||
// Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
|
|
||||||
#ifndef QMEDIAPLAYLIST_P_H
|
|
||||||
#define QMEDIAPLAYLIST_P_H
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G
|
|
||||||
// -------------
|
|
||||||
//
|
|
||||||
// This file is not part of the Qt API. It exists purely as an
|
|
||||||
// implementation detail. This header file may change from version to
|
|
||||||
// version without notice, or even be removed.
|
|
||||||
//
|
|
||||||
// We mean it.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "qmediaplaylist.h"
|
|
||||||
#include "qplaylistfileparser_p.h"
|
|
||||||
|
|
||||||
#include <QtCore/qdebug.h>
|
|
||||||
|
|
||||||
#ifdef Q_MOC_RUN
|
|
||||||
# pragma Q_MOC_EXPAND_MACROS
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
|
|
||||||
|
|
||||||
class QMediaPlaylistControl;
|
|
||||||
|
|
||||||
class QMediaPlaylistPrivate
|
|
||||||
{
|
|
||||||
Q_DECLARE_PUBLIC(QMediaPlaylist)
|
|
||||||
public:
|
|
||||||
QMediaPlaylistPrivate()
|
|
||||||
: error(QMediaPlaylist::NoError)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual ~QMediaPlaylistPrivate()
|
|
||||||
{
|
|
||||||
if (parser)
|
|
||||||
delete parser;
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadFailed(QMediaPlaylist::Error error, const QString &errorString)
|
|
||||||
{
|
|
||||||
this->error = error;
|
|
||||||
this->errorString = errorString;
|
|
||||||
|
|
||||||
emit q_ptr->loadFailed();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadFinished()
|
|
||||||
{
|
|
||||||
q_ptr->addMedia(parser->playlist);
|
|
||||||
|
|
||||||
emit q_ptr->loaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool checkFormat(const char *format) const
|
|
||||||
{
|
|
||||||
QLatin1String f(format);
|
|
||||||
QPlaylistFileParser::FileType type = format ? QPlaylistFileParser::UNKNOWN : QPlaylistFileParser::M3U8;
|
|
||||||
if (format) {
|
|
||||||
if (f == QLatin1String("m3u") || f == QLatin1String("text/uri-list") ||
|
|
||||||
f == QLatin1String("audio/x-mpegurl") || f == QLatin1String("audio/mpegurl"))
|
|
||||||
type = QPlaylistFileParser::M3U;
|
|
||||||
else if (f == QLatin1String("m3u8") || f == QLatin1String("application/x-mpegURL") ||
|
|
||||||
f == QLatin1String("application/vnd.apple.mpegurl"))
|
|
||||||
type = QPlaylistFileParser::M3U8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == QPlaylistFileParser::UNKNOWN || type == QPlaylistFileParser::PLS) {
|
|
||||||
error = QMediaPlaylist::FormatNotSupportedError;
|
|
||||||
errorString = QMediaPlaylist::tr("This file format is not supported.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ensureParser()
|
|
||||||
{
|
|
||||||
if (parser)
|
|
||||||
return;
|
|
||||||
|
|
||||||
parser = new QPlaylistFileParser(q_ptr);
|
|
||||||
QObject::connect(parser, &QPlaylistFileParser::finished, [this]() { loadFinished(); });
|
|
||||||
QObject::connect(parser, &QPlaylistFileParser::error,
|
|
||||||
[this](QMediaPlaylist::Error err, const QString& errorMsg) { loadFailed(err, errorMsg); });
|
|
||||||
}
|
|
||||||
|
|
||||||
int nextPosition(int steps) const;
|
|
||||||
int prevPosition(int steps) const;
|
|
||||||
|
|
||||||
QList<QUrl> playlist;
|
|
||||||
|
|
||||||
int currentPos = -1;
|
|
||||||
QMediaPlaylist::PlaybackMode playbackMode = QMediaPlaylist::Sequential;
|
|
||||||
|
|
||||||
QPlaylistFileParser *parser = nullptr;
|
|
||||||
mutable QMediaPlaylist::Error error;
|
|
||||||
mutable QString errorString;
|
|
||||||
|
|
||||||
QMediaPlaylist *q_ptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
|
|
||||||
#endif // QMEDIAPLAYLIST_P_H
|
|
@ -1,605 +0,0 @@
|
|||||||
// Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
|
|
||||||
#include "qplaylistfileparser_p.h"
|
|
||||||
#include <qfileinfo.h>
|
|
||||||
#include <QtCore/QDebug>
|
|
||||||
#include <QtCore/qiodevice.h>
|
|
||||||
#include <QtCore/qpointer.h>
|
|
||||||
#include <QtNetwork/QNetworkReply>
|
|
||||||
#include <QtNetwork/QNetworkRequest>
|
|
||||||
#include "qmediaplayer.h"
|
|
||||||
#include "qmediametadata.h"
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
class ParserBase
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit ParserBase(QPlaylistFileParser *parent)
|
|
||||||
: m_parent(parent)
|
|
||||||
, m_aborted(false)
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseLine(int lineIndex, const QString& line, const QUrl& root)
|
|
||||||
{
|
|
||||||
if (m_aborted)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const bool ok = parseLineImpl(lineIndex, line, root);
|
|
||||||
return ok && !m_aborted;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void abort() { m_aborted = true; }
|
|
||||||
virtual ~ParserBase() = default;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) = 0;
|
|
||||||
|
|
||||||
static QUrl expandToFullPath(const QUrl &root, const QString &line)
|
|
||||||
{
|
|
||||||
// On Linux, backslashes are not converted to forward slashes :/
|
|
||||||
if (line.startsWith(QLatin1String("//")) || line.startsWith(QLatin1String("\\\\"))) {
|
|
||||||
// Network share paths are not resolved
|
|
||||||
return QUrl::fromLocalFile(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url(line);
|
|
||||||
if (url.scheme().isEmpty()) {
|
|
||||||
// Resolve it relative to root
|
|
||||||
if (root.isLocalFile())
|
|
||||||
return QUrl::fromUserInput(line, root.adjusted(QUrl::RemoveFilename).toLocalFile(), QUrl::AssumeLocalFile);
|
|
||||||
return root.resolved(url);
|
|
||||||
}
|
|
||||||
if (url.scheme().length() == 1)
|
|
||||||
// Assume it's a drive letter for a Windows path
|
|
||||||
url = QUrl::fromLocalFile(line);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
void newItemFound(const QVariant& content) { Q_EMIT m_parent->newItem(content); }
|
|
||||||
|
|
||||||
|
|
||||||
QPlaylistFileParser *m_parent;
|
|
||||||
bool m_aborted;
|
|
||||||
};
|
|
||||||
|
|
||||||
class M3UParser : public ParserBase
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit M3UParser(QPlaylistFileParser *q)
|
|
||||||
: ParserBase(q)
|
|
||||||
, m_extendedFormat(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
Extended M3U directives
|
|
||||||
|
|
||||||
#EXTM3U - header - must be first line of file
|
|
||||||
#EXTINF - extra info - length (seconds), title
|
|
||||||
#EXTINF - extra info - length (seconds), artist '-' title
|
|
||||||
|
|
||||||
Example
|
|
||||||
|
|
||||||
#EXTM3U
|
|
||||||
#EXTINF:123, Sample artist - Sample title
|
|
||||||
C:\Documents and Settings\I\My Music\Sample.mp3
|
|
||||||
#EXTINF:321,Example Artist - Example title
|
|
||||||
C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg
|
|
||||||
|
|
||||||
*/
|
|
||||||
bool parseLineImpl(int lineIndex, const QString& line, const QUrl& root) override
|
|
||||||
{
|
|
||||||
if (line[0] == u'#' ) {
|
|
||||||
if (m_extendedFormat) {
|
|
||||||
if (line.startsWith(QLatin1String("#EXTINF:"))) {
|
|
||||||
m_extraInfo.clear();
|
|
||||||
int artistStart = line.indexOf(QLatin1String(","), 8);
|
|
||||||
bool ok = false;
|
|
||||||
QStringView lineView { line };
|
|
||||||
int length = lineView.mid(8, artistStart < 8 ? -1 : artistStart - 8).trimmed().toInt(&ok);
|
|
||||||
if (ok && length > 0) {
|
|
||||||
//convert from second to milisecond
|
|
||||||
m_extraInfo[QMediaMetaData::Duration] = QVariant(length * 1000);
|
|
||||||
}
|
|
||||||
if (artistStart > 0) {
|
|
||||||
int titleStart = getSplitIndex(line, artistStart);
|
|
||||||
if (titleStart > artistStart) {
|
|
||||||
m_extraInfo[QMediaMetaData::Author] = lineView.mid(artistStart + 1,
|
|
||||||
titleStart - artistStart - 1).trimmed().toString().
|
|
||||||
replace(QLatin1String("--"), QLatin1String("-"));
|
|
||||||
m_extraInfo[QMediaMetaData::Title] = lineView.mid(titleStart + 1).trimmed().toString().
|
|
||||||
replace(QLatin1String("--"), QLatin1String("-"));
|
|
||||||
} else {
|
|
||||||
m_extraInfo[QMediaMetaData::Title] = lineView.mid(artistStart + 1).trimmed().toString().
|
|
||||||
replace(QLatin1String("--"), QLatin1String("-"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (lineIndex == 0 && line.startsWith(QLatin1String("#EXTM3U"))) {
|
|
||||||
m_extendedFormat = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
QUrl url = expandToFullPath(root, line);
|
|
||||||
m_extraInfo[QMediaMetaData::Url] = url;
|
|
||||||
m_parent->playlist.append(url);
|
|
||||||
newItemFound(QVariant::fromValue(m_extraInfo));
|
|
||||||
m_extraInfo.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getSplitIndex(const QString& line, int startPos)
|
|
||||||
{
|
|
||||||
if (startPos < 0)
|
|
||||||
startPos = 0;
|
|
||||||
const QChar* buf = line.data();
|
|
||||||
for (int i = startPos; i < line.length(); ++i) {
|
|
||||||
if (buf[i] == u'-') {
|
|
||||||
if (i == line.length() - 1)
|
|
||||||
return i;
|
|
||||||
++i;
|
|
||||||
if (buf[i] != u'-')
|
|
||||||
return i - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QMediaMetaData m_extraInfo;
|
|
||||||
bool m_extendedFormat;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PLSParser : public ParserBase
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit PLSParser(QPlaylistFileParser *q)
|
|
||||||
: ParserBase(q)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
The format is essentially that of an INI file structured as follows:
|
|
||||||
|
|
||||||
Header
|
|
||||||
|
|
||||||
* [playlist] : This tag indicates that it is a Playlist File
|
|
||||||
|
|
||||||
Track Entry
|
|
||||||
Assuming track entry #X
|
|
||||||
|
|
||||||
* FileX : Variable defining location of stream.
|
|
||||||
* TitleX : Defines track title.
|
|
||||||
* LengthX : Length in seconds of track. Value of -1 indicates indefinite.
|
|
||||||
|
|
||||||
Footer
|
|
||||||
|
|
||||||
* NumberOfEntries : This variable indicates the number of tracks.
|
|
||||||
* Version : Playlist version. Currently only a value of 2 is valid.
|
|
||||||
|
|
||||||
[playlist]
|
|
||||||
|
|
||||||
File1=Alternative\everclear - SMFTA.mp3
|
|
||||||
|
|
||||||
Title1=Everclear - So Much For The Afterglow
|
|
||||||
|
|
||||||
Length1=233
|
|
||||||
|
|
||||||
File2=http://www.site.com:8000/listen.pls
|
|
||||||
|
|
||||||
Title2=My Cool Stream
|
|
||||||
|
|
||||||
Length5=-1
|
|
||||||
|
|
||||||
NumberOfEntries=2
|
|
||||||
|
|
||||||
Version=2
|
|
||||||
*/
|
|
||||||
bool parseLineImpl(int, const QString &line, const QUrl &root) override
|
|
||||||
{
|
|
||||||
// We ignore everything but 'File' entries, since that's the only thing we care about.
|
|
||||||
if (!line.startsWith(QLatin1String("File")))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
QString value = getValue(line);
|
|
||||||
if (value.isEmpty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
QUrl path = expandToFullPath(root, value);
|
|
||||||
m_parent->playlist.append(path);
|
|
||||||
newItemFound(path);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString getValue(QStringView line) {
|
|
||||||
int start = line.indexOf(u'=');
|
|
||||||
if (start < 0)
|
|
||||||
return QString();
|
|
||||||
return line.mid(start + 1).trimmed().toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
class QPlaylistFileParserPrivate
|
|
||||||
{
|
|
||||||
Q_DECLARE_PUBLIC(QPlaylistFileParser)
|
|
||||||
public:
|
|
||||||
QPlaylistFileParserPrivate(QPlaylistFileParser *q)
|
|
||||||
: q_ptr(q)
|
|
||||||
, m_stream(nullptr)
|
|
||||||
, m_type(QPlaylistFileParser::UNKNOWN)
|
|
||||||
, m_scanIndex(0)
|
|
||||||
, m_lineIndex(-1)
|
|
||||||
, m_utf8(false)
|
|
||||||
, m_aborted(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleData();
|
|
||||||
void handleParserFinished();
|
|
||||||
void abort();
|
|
||||||
void reset();
|
|
||||||
|
|
||||||
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> m_source;
|
|
||||||
QScopedPointer<ParserBase> m_currentParser;
|
|
||||||
QByteArray m_buffer;
|
|
||||||
QUrl m_root;
|
|
||||||
QNetworkAccessManager m_mgr;
|
|
||||||
QString m_mimeType;
|
|
||||||
QPlaylistFileParser *q_ptr;
|
|
||||||
QPointer<QIODevice> m_stream;
|
|
||||||
QPlaylistFileParser::FileType m_type;
|
|
||||||
struct ParserJob
|
|
||||||
{
|
|
||||||
QIODevice *m_stream;
|
|
||||||
QUrl m_media;
|
|
||||||
QString m_mimeType;
|
|
||||||
[[nodiscard]] bool isValid() const { return m_stream || !m_media.isEmpty(); }
|
|
||||||
void reset() { m_stream = nullptr; m_media = QUrl(); m_mimeType = QString(); }
|
|
||||||
} m_pendingJob;
|
|
||||||
int m_scanIndex;
|
|
||||||
int m_lineIndex;
|
|
||||||
bool m_utf8;
|
|
||||||
bool m_aborted;
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool processLine(int startIndex, int length);
|
|
||||||
};
|
|
||||||
|
|
||||||
#define LINE_LIMIT 4096
|
|
||||||
#define READ_LIMIT 64
|
|
||||||
|
|
||||||
bool QPlaylistFileParserPrivate::processLine(int startIndex, int length)
|
|
||||||
{
|
|
||||||
Q_Q(QPlaylistFileParser);
|
|
||||||
m_lineIndex++;
|
|
||||||
|
|
||||||
if (!m_currentParser) {
|
|
||||||
const QString urlString = m_root.toString();
|
|
||||||
const QString &suffix = !urlString.isEmpty() ? QFileInfo(urlString).suffix() : urlString;
|
|
||||||
QString mimeType;
|
|
||||||
if (m_source)
|
|
||||||
mimeType = m_source->header(QNetworkRequest::ContentTypeHeader).toString();
|
|
||||||
m_type = QPlaylistFileParser::findPlaylistType(suffix, !mimeType.isEmpty() ? mimeType : m_mimeType, m_buffer.constData(), quint32(m_buffer.size()));
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case QPlaylistFileParser::UNKNOWN:
|
|
||||||
emit q->error(QMediaPlaylist::FormatError,
|
|
||||||
QMediaPlaylist::tr("%1 playlist type is unknown").arg(m_root.toString()));
|
|
||||||
q->abort();
|
|
||||||
return false;
|
|
||||||
case QPlaylistFileParser::M3U:
|
|
||||||
m_currentParser.reset(new M3UParser(q));
|
|
||||||
break;
|
|
||||||
case QPlaylistFileParser::M3U8:
|
|
||||||
m_currentParser.reset(new M3UParser(q));
|
|
||||||
m_utf8 = true;
|
|
||||||
break;
|
|
||||||
case QPlaylistFileParser::PLS:
|
|
||||||
m_currentParser.reset(new PLSParser(q));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_ASSERT(!m_currentParser.isNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString line;
|
|
||||||
|
|
||||||
if (m_utf8) {
|
|
||||||
line = QString::fromUtf8(m_buffer.constData() + startIndex, length).trimmed();
|
|
||||||
} else {
|
|
||||||
line = QString::fromLatin1(m_buffer.constData() + startIndex, length).trimmed();
|
|
||||||
}
|
|
||||||
if (line.isEmpty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
Q_ASSERT(m_currentParser);
|
|
||||||
return m_currentParser->parseLine(m_lineIndex, line, m_root);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParserPrivate::handleData()
|
|
||||||
{
|
|
||||||
Q_Q(QPlaylistFileParser);
|
|
||||||
while (m_stream->bytesAvailable() && !m_aborted) {
|
|
||||||
int expectedBytes = qMin(READ_LIMIT, int(qMin(m_stream->bytesAvailable(),
|
|
||||||
qint64(LINE_LIMIT - m_buffer.size()))));
|
|
||||||
m_buffer.push_back(m_stream->read(expectedBytes));
|
|
||||||
int processedBytes = 0;
|
|
||||||
while (m_scanIndex < m_buffer.length() && !m_aborted) {
|
|
||||||
char s = m_buffer[m_scanIndex];
|
|
||||||
if (s == '\r' || s == '\n') {
|
|
||||||
int l = m_scanIndex - processedBytes;
|
|
||||||
if (l > 0) {
|
|
||||||
if (!processLine(processedBytes, l))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
processedBytes = m_scanIndex + 1;
|
|
||||||
if (!m_stream) {
|
|
||||||
//some error happened, so exit parsing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_scanIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_aborted)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (m_buffer.length() - processedBytes >= LINE_LIMIT) {
|
|
||||||
emit q->error(QMediaPlaylist::FormatError, QMediaPlaylist::tr("invalid line in playlist file"));
|
|
||||||
q->abort();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_stream->bytesAvailable() && (!m_source || !m_source->isFinished())) {
|
|
||||||
//last line
|
|
||||||
processLine(processedBytes, -1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_ASSERT(m_buffer.length() == m_scanIndex);
|
|
||||||
if (processedBytes == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int copyLength = m_buffer.length() - processedBytes;
|
|
||||||
if (copyLength > 0) {
|
|
||||||
Q_ASSERT(copyLength <= READ_LIMIT);
|
|
||||||
m_buffer = m_buffer.right(copyLength);
|
|
||||||
} else {
|
|
||||||
m_buffer.clear();
|
|
||||||
}
|
|
||||||
m_scanIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleParserFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlaylistFileParser::QPlaylistFileParser(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, d_ptr(new QPlaylistFileParserPrivate(this))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlaylistFileParser::~QPlaylistFileParser() = default;
|
|
||||||
|
|
||||||
QPlaylistFileParser::FileType QPlaylistFileParser::findByMimeType(const QString &mime)
|
|
||||||
{
|
|
||||||
if (mime == QLatin1String("text/uri-list") || mime == QLatin1String("audio/x-mpegurl") || mime == QLatin1String("audio/mpegurl"))
|
|
||||||
return QPlaylistFileParser::M3U;
|
|
||||||
|
|
||||||
if (mime == QLatin1String("application/x-mpegURL") || mime == QLatin1String("application/vnd.apple.mpegurl"))
|
|
||||||
return QPlaylistFileParser::M3U8;
|
|
||||||
|
|
||||||
if (mime == QLatin1String("audio/x-scpls"))
|
|
||||||
return QPlaylistFileParser::PLS;
|
|
||||||
|
|
||||||
return QPlaylistFileParser::UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlaylistFileParser::FileType QPlaylistFileParser::findBySuffixType(const QString &suffix)
|
|
||||||
{
|
|
||||||
const QString &s = suffix.toLower();
|
|
||||||
|
|
||||||
if (s == QLatin1String("m3u"))
|
|
||||||
return QPlaylistFileParser::M3U;
|
|
||||||
|
|
||||||
if (s == QLatin1String("m3u8"))
|
|
||||||
return QPlaylistFileParser::M3U8;
|
|
||||||
|
|
||||||
if (s == QLatin1String("pls"))
|
|
||||||
return QPlaylistFileParser::PLS;
|
|
||||||
|
|
||||||
return QPlaylistFileParser::UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlaylistFileParser::FileType QPlaylistFileParser::findByDataHeader(const char *data, quint32 size)
|
|
||||||
{
|
|
||||||
if (!data || size == 0)
|
|
||||||
return QPlaylistFileParser::UNKNOWN;
|
|
||||||
|
|
||||||
if (size >= 7 && strncmp(data, "#EXTM3U", 7) == 0)
|
|
||||||
return QPlaylistFileParser::M3U;
|
|
||||||
|
|
||||||
if (size >= 10 && strncmp(data, "[playlist]", 10) == 0)
|
|
||||||
return QPlaylistFileParser::PLS;
|
|
||||||
|
|
||||||
return QPlaylistFileParser::UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlaylistFileParser::FileType QPlaylistFileParser::findPlaylistType(const QString& suffix,
|
|
||||||
const QString& mime,
|
|
||||||
const char *data,
|
|
||||||
quint32 size)
|
|
||||||
{
|
|
||||||
|
|
||||||
FileType dataHeaderType = findByDataHeader(data, size);
|
|
||||||
if (dataHeaderType != UNKNOWN)
|
|
||||||
return dataHeaderType;
|
|
||||||
|
|
||||||
FileType mimeType = findByMimeType(mime);
|
|
||||||
if (mimeType != UNKNOWN)
|
|
||||||
return mimeType;
|
|
||||||
|
|
||||||
mimeType = findBySuffixType(mime);
|
|
||||||
if (mimeType != UNKNOWN)
|
|
||||||
return mimeType;
|
|
||||||
|
|
||||||
FileType suffixType = findBySuffixType(suffix);
|
|
||||||
if (suffixType != UNKNOWN)
|
|
||||||
return suffixType;
|
|
||||||
|
|
||||||
return UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delegating
|
|
||||||
*/
|
|
||||||
void QPlaylistFileParser::start(const QUrl &media, QIODevice *stream, const QString &mimeType)
|
|
||||||
{
|
|
||||||
if (stream)
|
|
||||||
start(stream, mimeType);
|
|
||||||
else
|
|
||||||
start(media, mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParser::start(QIODevice *stream, const QString &mimeType)
|
|
||||||
{
|
|
||||||
Q_D(QPlaylistFileParser);
|
|
||||||
const bool validStream = stream ? (stream->isOpen() && stream->isReadable()) : false;
|
|
||||||
|
|
||||||
if (!validStream) {
|
|
||||||
Q_EMIT error(QMediaPlaylist::AccessDeniedError, QMediaPlaylist::tr("Invalid stream"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!d->m_currentParser.isNull()) {
|
|
||||||
abort();
|
|
||||||
d->m_pendingJob = { stream, QUrl(), mimeType };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist.clear();
|
|
||||||
d->reset();
|
|
||||||
d->m_mimeType = mimeType;
|
|
||||||
d->m_stream = stream;
|
|
||||||
connect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
|
|
||||||
d->handleData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParser::start(const QUrl& request, const QString &mimeType)
|
|
||||||
{
|
|
||||||
Q_D(QPlaylistFileParser);
|
|
||||||
const QUrl &url = request.url();
|
|
||||||
|
|
||||||
if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
|
|
||||||
emit error(QMediaPlaylist::AccessDeniedError, QString(QMediaPlaylist::tr("%1 does not exist")).arg(url.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!d->m_currentParser.isNull()) {
|
|
||||||
abort();
|
|
||||||
d->m_pendingJob = { nullptr, request, mimeType };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
d->reset();
|
|
||||||
d->m_root = url;
|
|
||||||
d->m_mimeType = mimeType;
|
|
||||||
d->m_source.reset(d->m_mgr.get(QNetworkRequest(request)));
|
|
||||||
d->m_stream = d->m_source.get();
|
|
||||||
connect(d->m_source.data(), SIGNAL(readyRead()), this, SLOT(handleData()));
|
|
||||||
connect(d->m_source.data(), SIGNAL(finished()), this, SLOT(handleData()));
|
|
||||||
connect(d->m_source.data(), SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(handleError()));
|
|
||||||
|
|
||||||
if (url.isLocalFile())
|
|
||||||
d->handleData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParser::abort()
|
|
||||||
{
|
|
||||||
Q_D(QPlaylistFileParser);
|
|
||||||
d->abort();
|
|
||||||
|
|
||||||
if (d->m_source)
|
|
||||||
d->m_source->disconnect();
|
|
||||||
|
|
||||||
if (d->m_stream)
|
|
||||||
disconnect(d->m_stream, SIGNAL(readyRead()), this, SLOT(handleData()));
|
|
||||||
|
|
||||||
playlist.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParser::handleData()
|
|
||||||
{
|
|
||||||
Q_D(QPlaylistFileParser);
|
|
||||||
d->handleData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParserPrivate::handleParserFinished()
|
|
||||||
{
|
|
||||||
Q_Q(QPlaylistFileParser);
|
|
||||||
const bool isParserValid = !m_currentParser.isNull();
|
|
||||||
if (!isParserValid && !m_aborted)
|
|
||||||
emit q->error(QMediaPlaylist::FormatNotSupportedError, QMediaPlaylist::tr("Empty file provided"));
|
|
||||||
|
|
||||||
if (isParserValid && !m_aborted) {
|
|
||||||
m_currentParser.reset();
|
|
||||||
emit q->finished();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_aborted)
|
|
||||||
q->abort();
|
|
||||||
|
|
||||||
if (!m_source.isNull())
|
|
||||||
m_source.reset();
|
|
||||||
|
|
||||||
if (m_pendingJob.isValid())
|
|
||||||
q->start(m_pendingJob.m_media, m_pendingJob.m_stream, m_pendingJob.m_mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParserPrivate::abort()
|
|
||||||
{
|
|
||||||
m_aborted = true;
|
|
||||||
if (!m_currentParser.isNull())
|
|
||||||
m_currentParser->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParserPrivate::reset()
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_currentParser.isNull());
|
|
||||||
Q_ASSERT(m_source.isNull());
|
|
||||||
m_buffer.clear();
|
|
||||||
m_root.clear();
|
|
||||||
m_mimeType.clear();
|
|
||||||
m_stream = nullptr;
|
|
||||||
m_type = QPlaylistFileParser::UNKNOWN;
|
|
||||||
m_scanIndex = 0;
|
|
||||||
m_lineIndex = -1;
|
|
||||||
m_utf8 = false;
|
|
||||||
m_aborted = false;
|
|
||||||
m_pendingJob.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QPlaylistFileParser::handleError()
|
|
||||||
{
|
|
||||||
Q_D(QPlaylistFileParser);
|
|
||||||
const QString &errorString = d->m_source->errorString();
|
|
||||||
Q_EMIT error(QMediaPlaylist::NetworkError, errorString);
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
|
@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2016 The Qt Company Ltd.
|
|
||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
|
|
||||||
#ifndef PLAYLISTFILEPARSER_P_H
|
|
||||||
#define PLAYLISTFILEPARSER_P_H
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G
|
|
||||||
// -------------
|
|
||||||
//
|
|
||||||
// This file is not part of the Qt API. It exists purely as an
|
|
||||||
// implementation detail. This header file may change from version to
|
|
||||||
// version without notice, or even be removed.
|
|
||||||
//
|
|
||||||
// We mean it.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "qtmultimediaglobal.h"
|
|
||||||
#include "qmediaplaylist.h"
|
|
||||||
#include <QtCore/qobject.h>
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
|
|
||||||
class QIODevice;
|
|
||||||
class QUrl;
|
|
||||||
class QNetworkRequest;
|
|
||||||
|
|
||||||
class QPlaylistFileParserPrivate;
|
|
||||||
|
|
||||||
class QPlaylistFileParser : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
QPlaylistFileParser(QObject *parent = nullptr);
|
|
||||||
~QPlaylistFileParser();
|
|
||||||
|
|
||||||
enum FileType
|
|
||||||
{
|
|
||||||
UNKNOWN,
|
|
||||||
M3U,
|
|
||||||
M3U8, // UTF-8 version of M3U
|
|
||||||
PLS
|
|
||||||
};
|
|
||||||
|
|
||||||
void start(const QUrl &media, QIODevice *stream = nullptr, const QString &mimeType = QString());
|
|
||||||
void start(const QUrl &request, const QString &mimeType = QString());
|
|
||||||
void start(QIODevice *stream, const QString &mimeType = QString());
|
|
||||||
void abort();
|
|
||||||
|
|
||||||
QList<QUrl> playlist;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void newItem(const QVariant& content);
|
|
||||||
void finished();
|
|
||||||
void error(QMediaPlaylist::Error err, const QString& errorMsg);
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void handleData();
|
|
||||||
void handleError();
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
static FileType findByMimeType(const QString &mime);
|
|
||||||
static FileType findBySuffixType(const QString &suffix);
|
|
||||||
static FileType findByDataHeader(const char *data, quint32 size);
|
|
||||||
static FileType findPlaylistType(QIODevice *device,
|
|
||||||
const QString& mime);
|
|
||||||
static FileType findPlaylistType(const QString &suffix,
|
|
||||||
const QString& mime,
|
|
||||||
const char *data = nullptr,
|
|
||||||
quint32 size = 0);
|
|
||||||
|
|
||||||
Q_DISABLE_COPY(QPlaylistFileParser)
|
|
||||||
Q_DECLARE_PRIVATE(QPlaylistFileParser)
|
|
||||||
QScopedPointer<QPlaylistFileParserPrivate> d_ptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
#endif // PLAYLISTFILEPARSER_P_H
|
|
@ -15,6 +15,8 @@
|
|||||||
<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/skin.png</file>
|
||||||
<file>icons/media-album-cover.svg</file>
|
<file>icons/media-album-cover.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
@ -1,29 +1,18 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#include "seekableslider.h"
|
#include "seekableslider.h"
|
||||||
|
|
||||||
SeekableSlider::SeekableSlider(QWidget *parent) :
|
SeekableSlider::SeekableSlider(QWidget *parent) :
|
||||||
QSlider(parent)
|
QSlider(parent)
|
||||||
{
|
{
|
||||||
//关闭分段移动
|
|
||||||
setSingleStep(0);
|
|
||||||
setPageStep(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//点击Slider即可调节Value
|
void SeekableSlider::mouseReleaseEvent(QMouseEvent *event)
|
||||||
//只写了横向模式的Slider……
|
|
||||||
void SeekableSlider::mousePressEvent(QMouseEvent *event)
|
|
||||||
{
|
{
|
||||||
if (event->buttons() & Qt::LeftButton) {
|
|
||||||
QSlider::mousePressEvent(event);
|
|
||||||
double pos = event->pos().x() / (double)width();
|
double pos = event->pos().x() / (double)width();
|
||||||
setValue(pos * (maximum() - minimum()) + minimum());
|
setValue(pos * (maximum() - minimum()) + minimum());
|
||||||
}
|
emit sliderReleased();
|
||||||
}
|
return QSlider::mouseReleaseEvent(event);
|
||||||
|
|
||||||
void SeekableSlider::mouseMoveEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
if (event->buttons() & Qt::LeftButton) {
|
|
||||||
QSlider::mousePressEvent(event);
|
|
||||||
double pos = event->pos().x() / (double)width();
|
|
||||||
setValue(pos * (maximum() - minimum()) + minimum());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
@ -8,13 +12,13 @@ class SeekableSlider : public QSlider
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit SeekableSlider(QWidget *parent = nullptr);
|
explicit SeekableSlider(QWidget *parent = nullptr);
|
||||||
|
~SeekableSlider() override = default;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#include "singleapplicationmanager.h"
|
#include "singleapplicationmanager.h"
|
||||||
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#ifndef SINGLEAPPLICATIONMANAGER_H
|
#ifndef SINGLEAPPLICATIONMANAGER_H
|
||||||
#define SINGLEAPPLICATIONMANAGER_H
|
#define SINGLEAPPLICATIONMANAGER_H
|
||||||
|
|
||||||
|
383
taskbarmanager.cpp
Normal file
383
taskbarmanager.cpp
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 (c) Jack Hill <jackhill3103@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 (c) Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This file is modified based on Elisa's taskmanager.cpp implementation,
|
||||||
|
// with its Qt Quick usage removed.
|
||||||
|
|
||||||
|
#include "taskbarmanager.h"
|
||||||
|
|
||||||
|
#include <QAbstractEventDispatcher>
|
||||||
|
#include <QAbstractNativeEventFilter>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <Shobjidl.h>
|
||||||
|
#include <WinDef.h>
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
constexpr int numOfButtons = 3;
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
static HICON hiconFromTheme(const QString &iconName)
|
||||||
|
#else
|
||||||
|
static HICON hiconFromTheme(const QIcon::ThemeIcon iconName)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
const auto width = GetSystemMetrics(SM_CXSMICON);
|
||||||
|
const auto height = GetSystemMetrics(SM_CYSMICON);
|
||||||
|
return QIcon::fromTheme(iconName).pixmap(width, height).toImage().toHICON();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskBarManagerPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
ITaskbarList3 *taskBar = nullptr;
|
||||||
|
HWND hwnd = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the toolbar has been added to the taskbar
|
||||||
|
*/
|
||||||
|
bool addedThumbButtons = false;
|
||||||
|
|
||||||
|
QMediaPlayer::PlaybackState playbackState = QMediaPlayer::StoppedState;
|
||||||
|
|
||||||
|
bool showProgress = false;
|
||||||
|
qlonglong progressMaximum = 0;
|
||||||
|
qlonglong progressValue = 0;
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
|
||||||
|
HICON skipBackwardIcon = hiconFromTheme(u"media-skip-backward"_s);
|
||||||
|
HICON skipForwardIcon = hiconFromTheme(u"media-skip-forward"_s);
|
||||||
|
HICON playIcon = hiconFromTheme(u"media-playback-start"_s);
|
||||||
|
HICON pauseIcon = hiconFromTheme(u"media-playback-pause"_s);
|
||||||
|
#else
|
||||||
|
HICON skipBackwardIcon = hiconFromTheme(QIcon::ThemeIcon::MediaSkipBackward);
|
||||||
|
HICON skipForwardIcon = hiconFromTheme(QIcon::ThemeIcon::MediaSkipForward);
|
||||||
|
HICON playIcon = hiconFromTheme(QIcon::ThemeIcon::MediaPlaybackStart);
|
||||||
|
HICON pauseIcon = hiconFromTheme(QIcon::ThemeIcon::MediaPlaybackPause);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the current state of each button
|
||||||
|
*/
|
||||||
|
struct ButtonData {
|
||||||
|
HICON icon = nullptr;
|
||||||
|
bool enabled = false;
|
||||||
|
};
|
||||||
|
std::array<ButtonData, numOfButtons> buttonData = {{
|
||||||
|
{skipBackwardIcon, false},
|
||||||
|
{playIcon, false},
|
||||||
|
{skipForwardIcon, false}
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For convenience when reading code
|
||||||
|
*/
|
||||||
|
enum ButtonID {
|
||||||
|
SkipBackward = 0,
|
||||||
|
TogglePlayback = 1,
|
||||||
|
SkipForward = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskBarManagerPrivate()
|
||||||
|
{
|
||||||
|
HRESULT result = CoCreateInstance(CLSID_TaskbarList,
|
||||||
|
nullptr,
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(&taskBar));
|
||||||
|
if (FAILED(result)) {
|
||||||
|
taskBar = nullptr;
|
||||||
|
qWarning() << "Failed to create Windows taskbar instance";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~TaskBarManagerPrivate()
|
||||||
|
{
|
||||||
|
if (taskBar) {
|
||||||
|
taskBar->Release();
|
||||||
|
}
|
||||||
|
DestroyIcon(skipForwardIcon);
|
||||||
|
DestroyIcon(skipBackwardIcon);
|
||||||
|
DestroyIcon(pauseIcon);
|
||||||
|
DestroyIcon(playIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the native window handle for the taskbar instance
|
||||||
|
*/
|
||||||
|
void setHWND(HWND newHwnd)
|
||||||
|
{
|
||||||
|
if (hwnd == newHwnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hwnd = newHwnd;
|
||||||
|
addedThumbButtons = false;
|
||||||
|
if (hwnd) {
|
||||||
|
updateProgressFlags();
|
||||||
|
updateProgressValue();
|
||||||
|
setupTaskBarManagerButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the taskbar progress flags
|
||||||
|
*/
|
||||||
|
void updateProgressFlags()
|
||||||
|
{
|
||||||
|
if (taskBar && hwnd) {
|
||||||
|
taskBar->SetProgressState(hwnd, progressFlags());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the taskbar progress value, only if the taskbar is showing progress
|
||||||
|
*/
|
||||||
|
void updateProgressValue()
|
||||||
|
{
|
||||||
|
if (taskBar && hwnd && showProgress && playbackState != QMediaPlayer::StoppedState) {
|
||||||
|
taskBar->SetProgressValue(hwnd, progressValue, progressMaximum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state of a single toolbutton
|
||||||
|
*/
|
||||||
|
void updateButton(const ButtonID buttonId)
|
||||||
|
{
|
||||||
|
if (!taskBar || !hwnd || !addedThumbButtons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
THUMBBUTTON thumbButtons[1];
|
||||||
|
initThumbButton(thumbButtons, buttonId);
|
||||||
|
taskBar->ThumbBarUpdateButtons(hwnd, 1, thumbButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Convert playbackState into windows taskbar flags
|
||||||
|
*/
|
||||||
|
TBPFLAG progressFlags() const
|
||||||
|
{
|
||||||
|
if (!showProgress) {
|
||||||
|
return TBPF_NOPROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (playbackState) {
|
||||||
|
case QMediaPlayer::StoppedState:
|
||||||
|
return TBPF_NOPROGRESS;
|
||||||
|
case QMediaPlayer::PlayingState:
|
||||||
|
return TBPF_NORMAL;
|
||||||
|
case QMediaPlayer::PausedState:
|
||||||
|
return TBPF_PAUSED;
|
||||||
|
default:
|
||||||
|
return TBPF_NOPROGRESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a single toolbutton
|
||||||
|
*/
|
||||||
|
void initThumbButton(THUMBBUTTON *button, const ButtonID id)
|
||||||
|
{
|
||||||
|
button->dwMask = THB_ICON | THB_FLAGS;
|
||||||
|
button->iId = id;
|
||||||
|
button->hIcon = buttonData.at(id).icon;
|
||||||
|
button->dwFlags = buttonData.at(id).enabled ? THBF_ENABLED : THBF_DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the toolbar
|
||||||
|
*/
|
||||||
|
void setupTaskBarManagerButtons()
|
||||||
|
{
|
||||||
|
if (!taskBar || !hwnd || addedThumbButtons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
THUMBBUTTON thumbButtons[numOfButtons];
|
||||||
|
THUMBBUTTON *currButton = thumbButtons;
|
||||||
|
for (int i = 0; i < numOfButtons; ++i) {
|
||||||
|
initThumbButton(currButton++, static_cast<ButtonID>(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// win32 api reference says we should pass &thumbButtons but this doesn't compile
|
||||||
|
HRESULT result = taskBar->ThumbBarAddButtons(hwnd, numOfButtons, thumbButtons);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
qWarning() << "Failed to create Windows taskbar buttons";
|
||||||
|
} else {
|
||||||
|
addedThumbButtons = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskBarManager::TaskBarManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, d(std::make_unique<TaskBarManagerPrivate>())
|
||||||
|
{
|
||||||
|
QAbstractEventDispatcher::instance()->installNativeEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBarManager::~TaskBarManager() = default;
|
||||||
|
|
||||||
|
bool TaskBarManager::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
|
||||||
|
{
|
||||||
|
if (eventType == "windows_generic_MSG") {
|
||||||
|
MSG *msg = static_cast<MSG *>(message);
|
||||||
|
if (msg->hwnd != d->hwnd || msg->message != WM_COMMAND || HIWORD(msg->wParam) != THBN_CLICKED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (LOWORD(msg->wParam)) {
|
||||||
|
case TaskBarManagerPrivate::SkipBackward:
|
||||||
|
Q_EMIT skipBackward();
|
||||||
|
return true;
|
||||||
|
case TaskBarManagerPrivate::TogglePlayback:
|
||||||
|
Q_EMIT togglePlayback();
|
||||||
|
return true;
|
||||||
|
case TaskBarManagerPrivate::SkipForward:
|
||||||
|
Q_EMIT skipForward();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMediaPlayer::PlaybackState TaskBarManager::playbackState() const
|
||||||
|
{
|
||||||
|
return d->playbackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::showProgress() const
|
||||||
|
{
|
||||||
|
return d->showProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
qulonglong TaskBarManager::progressMaximum() const
|
||||||
|
{
|
||||||
|
return d->progressMaximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
qulonglong TaskBarManager::progressValue() const
|
||||||
|
{
|
||||||
|
return d->progressValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canSkipBackward() const
|
||||||
|
{
|
||||||
|
return d->buttonData.at(TaskBarManagerPrivate::SkipBackward).enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canSkipForward() const
|
||||||
|
{
|
||||||
|
return d->buttonData.at(TaskBarManagerPrivate::SkipForward).enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canTogglePlayback() const
|
||||||
|
{
|
||||||
|
return d->buttonData.at(TaskBarManagerPrivate::TogglePlayback).enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setWinId(WId winId)
|
||||||
|
{
|
||||||
|
d->setHWND(reinterpret_cast<HWND>(winId));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setPlaybackState(const QMediaPlayer::PlaybackState newPlaybackState)
|
||||||
|
{
|
||||||
|
if (d->playbackState == newPlaybackState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->playbackState = newPlaybackState;
|
||||||
|
Q_EMIT playbackStateChanged();
|
||||||
|
|
||||||
|
d->buttonData.at(TaskBarManagerPrivate::TogglePlayback).icon =
|
||||||
|
d->playbackState == QMediaPlayer::PlayingState ? d->pauseIcon : d->playIcon;
|
||||||
|
d->updateButton(TaskBarManagerPrivate::TogglePlayback);
|
||||||
|
d->updateProgressFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setShowProgress(const bool showProgress)
|
||||||
|
{
|
||||||
|
if (d->showProgress == showProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->showProgress = showProgress;
|
||||||
|
Q_EMIT showProgressChanged();
|
||||||
|
|
||||||
|
d->updateProgressFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setProgressMaximum(const qlonglong newMaximum)
|
||||||
|
{
|
||||||
|
if (d->progressMaximum == newMaximum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->progressMaximum = newMaximum;
|
||||||
|
Q_EMIT progressMaximumChanged();
|
||||||
|
|
||||||
|
if (d->progressValue > d->progressMaximum) {
|
||||||
|
d->progressValue = d->progressMaximum;
|
||||||
|
Q_EMIT progressValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
d->updateProgressValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setProgressValue(const qlonglong newValue)
|
||||||
|
{
|
||||||
|
if (d->progressValue == newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->progressValue = newValue < d->progressMaximum ? newValue : d->progressMaximum;
|
||||||
|
Q_EMIT progressValueChanged();
|
||||||
|
|
||||||
|
d->updateProgressValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanSkipBackward(const bool canSkip)
|
||||||
|
{
|
||||||
|
if (d->buttonData[TaskBarManagerPrivate::SkipBackward].enabled == canSkip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->buttonData[TaskBarManagerPrivate::SkipBackward].enabled = canSkip;
|
||||||
|
Q_EMIT canSkipBackwardChanged();
|
||||||
|
|
||||||
|
d->updateButton(TaskBarManagerPrivate::SkipBackward);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanSkipForward(const bool canSkip)
|
||||||
|
{
|
||||||
|
if (d->buttonData[TaskBarManagerPrivate::SkipForward].enabled == canSkip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->buttonData[TaskBarManagerPrivate::SkipForward].enabled = canSkip;
|
||||||
|
Q_EMIT canSkipForwardChanged();
|
||||||
|
|
||||||
|
d->updateButton(TaskBarManagerPrivate::SkipForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanTogglePlayback(const bool canToggle)
|
||||||
|
{
|
||||||
|
if (d->buttonData[TaskBarManagerPrivate::TogglePlayback].enabled == canToggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->buttonData[TaskBarManagerPrivate::TogglePlayback].enabled = canToggle;
|
||||||
|
Q_EMIT canTogglePlaybackChanged();
|
||||||
|
|
||||||
|
d->updateButton(TaskBarManagerPrivate::TogglePlayback);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_taskbarmanager.cpp"
|
151
taskbarmanager.h
Normal file
151
taskbarmanager.h
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 (c) Jack Hill <jackhill3103@gmail.com>
|
||||||
|
// SPDX-FileCopyrightText: 2025 (c) Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This file is modified based on Elisa's taskmanager.cpp implementation,
|
||||||
|
// with its Qt Quick usage removed.
|
||||||
|
|
||||||
|
#ifndef TASKBAR_H
|
||||||
|
#define TASKBAR_H
|
||||||
|
|
||||||
|
#include <QAbstractNativeEventFilter>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class TaskBarManagerPrivate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows taskbar
|
||||||
|
*/
|
||||||
|
class TaskBarManager : public QObject, public QAbstractNativeEventFilter
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the media is playing, paused, or stopped
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(QMediaPlayer::PlaybackState playbackState
|
||||||
|
READ playbackState
|
||||||
|
WRITE setPlaybackState
|
||||||
|
NOTIFY playbackStateChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show track progress on the taskbar
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool showProgress
|
||||||
|
READ showProgress
|
||||||
|
WRITE setShowProgress
|
||||||
|
NOTIFY showProgressChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum possible progress
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(qulonglong progressMaximum
|
||||||
|
READ progressMaximum
|
||||||
|
WRITE setProgressMaximum
|
||||||
|
NOTIFY progressMaximumChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current progress; this is always clamped to be in the range [0, progressMaximum]
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(qulonglong progressValue
|
||||||
|
READ progressValue
|
||||||
|
WRITE setProgressValue
|
||||||
|
NOTIFY progressValueChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Skip Backward" button is enabled
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canSkipBackward
|
||||||
|
READ canSkipBackward
|
||||||
|
WRITE setCanSkipBackward
|
||||||
|
NOTIFY canSkipBackwardChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Skip Forward" button is enabled
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canSkipForward
|
||||||
|
READ canSkipForward
|
||||||
|
WRITE setCanSkipForward
|
||||||
|
NOTIFY canSkipForwardChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the "Toggle Playback" button is enabled
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canTogglePlayback
|
||||||
|
READ canTogglePlayback
|
||||||
|
WRITE setCanTogglePlayback
|
||||||
|
NOTIFY canTogglePlaybackChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit TaskBarManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
~TaskBarManager() override;
|
||||||
|
|
||||||
|
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||||
|
|
||||||
|
[[nodiscard]] QMediaPlayer::PlaybackState playbackState() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool showProgress() const;
|
||||||
|
|
||||||
|
[[nodiscard]] qulonglong progressMaximum() const;
|
||||||
|
|
||||||
|
[[nodiscard]] qulonglong progressValue() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool canSkipBackward() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool canSkipForward() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool canTogglePlayback() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
|
||||||
|
void playbackStateChanged();
|
||||||
|
|
||||||
|
void showProgressChanged();
|
||||||
|
|
||||||
|
void progressMaximumChanged();
|
||||||
|
|
||||||
|
void progressValueChanged();
|
||||||
|
|
||||||
|
void canSkipBackwardChanged();
|
||||||
|
|
||||||
|
void canSkipForwardChanged();
|
||||||
|
|
||||||
|
void canTogglePlaybackChanged();
|
||||||
|
|
||||||
|
void skipBackward();
|
||||||
|
|
||||||
|
void skipForward();
|
||||||
|
|
||||||
|
void togglePlayback();
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
|
||||||
|
void setWinId(WId win);
|
||||||
|
|
||||||
|
void setPlaybackState(QMediaPlayer::PlaybackState newPlaybackState);
|
||||||
|
|
||||||
|
void setShowProgress(bool showProgress);
|
||||||
|
|
||||||
|
void setProgressMaximum(qlonglong newMaximum);
|
||||||
|
|
||||||
|
void setProgressValue(qlonglong newValue);
|
||||||
|
|
||||||
|
void setCanSkipBackward(bool canSkip);
|
||||||
|
|
||||||
|
void setCanSkipForward(bool canSkip);
|
||||||
|
|
||||||
|
void setCanTogglePlayback(bool canToggle);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::unique_ptr<TaskBarManagerPrivate> d;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TASKBAR_H
|
97
taskbarmanager_dummy.cpp
Normal file
97
taskbarmanager_dummy.cpp
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "taskbarmanager.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
class TaskBarManagerPrivate {};
|
||||||
|
|
||||||
|
TaskBarManager::TaskBarManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskBarManager::~TaskBarManager() = default;
|
||||||
|
|
||||||
|
bool TaskBarManager::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMediaPlayer::PlaybackState TaskBarManager::playbackState() const
|
||||||
|
{
|
||||||
|
return QMediaPlayer::StoppedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::showProgress() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qulonglong TaskBarManager::progressMaximum() const
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
qulonglong TaskBarManager::progressValue() const
|
||||||
|
{
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canSkipBackward() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canSkipForward() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskBarManager::canTogglePlayback() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setWinId(WId winId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setPlaybackState(const QMediaPlayer::PlaybackState newPlaybackState)
|
||||||
|
{
|
||||||
|
Q_UNUSED(newPlaybackState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setShowProgress(const bool showProgress)
|
||||||
|
{
|
||||||
|
Q_UNUSED(showProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setProgressMaximum(const qlonglong newMaximum)
|
||||||
|
{
|
||||||
|
Q_UNUSED(newMaximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setProgressValue(const qlonglong newValue)
|
||||||
|
{
|
||||||
|
Q_UNUSED(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanSkipBackward(const bool canSkip)
|
||||||
|
{
|
||||||
|
Q_UNUSED(canSkip);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanSkipForward(const bool canSkip)
|
||||||
|
{
|
||||||
|
Q_UNUSED(canSkip);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskBarManager::setCanTogglePlayback(const bool canToggle)
|
||||||
|
{
|
||||||
|
Q_UNUSED(canToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_taskbarmanager.cpp"
|
Reference in New Issue
Block a user