Compare commits
433 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2f5486987 | ||
|
|
b0d390aaf1 | ||
|
|
bae1b42394 | ||
|
|
a2533edd57 | ||
|
|
7b88c198fe | ||
|
|
c49cb0c119 | ||
|
|
03aabeb848 | ||
|
|
1d3a837f7a | ||
|
|
92adc18b8f | ||
|
|
e4697c8ff1 | ||
|
|
b741f1a580 | ||
|
|
8ee6de4162 | ||
|
|
2b9e7db924 | ||
|
|
49384ce294 | ||
|
|
ac59fff346 | ||
|
|
a3e9f152d8 | ||
|
|
ffe6a81b9a | ||
|
|
9bb051b4eb | ||
|
|
c1465a890f | ||
|
|
c3ee3318d7 | ||
|
|
272976f0b7 | ||
|
|
4724b170b1 | ||
|
|
337bd4bcef | ||
|
|
0604c78453 | ||
|
|
c8169adf7c | ||
|
|
7b610d131c | ||
|
|
18da55453f | ||
|
|
2aeab8b672 | ||
|
|
9c21707a55 | ||
|
|
b02ac833ad | ||
|
|
4de912cf41 | ||
|
|
ed260c6e20 | ||
|
|
fab38f693d | ||
|
|
aedbd52e9d | ||
|
|
4cbcb9d99c | ||
|
|
e967d15b4e | ||
|
|
bb43cc63ec | ||
|
|
ca176c319d | ||
|
|
17c960ecd4 | ||
|
|
b766ae3498 | ||
|
|
faf4c817cd | ||
|
|
b4e1f283c9 | ||
|
|
52c83d592c | ||
|
|
c8caea0d30 | ||
|
|
7a6c54d8e7 | ||
|
|
7082e52a4f | ||
|
|
6cdca617e0 | ||
|
|
a1adc1a75a | ||
|
|
b16bec704a | ||
|
|
664a8c79a1 | ||
|
|
bbf3d8e1a4 | ||
|
|
a35146440c | ||
|
|
8e79fafca9 | ||
|
|
6f8780d3cc | ||
|
|
264065a355 | ||
|
|
b606e4cd1a | ||
|
|
01d0eeaed0 | ||
|
|
8c4e24e65d | ||
|
|
19c9f9698d | ||
|
|
ae87c1b578 | ||
|
|
c42b1f5548 | ||
|
|
87ffbb0a85 | ||
|
|
702851a958 | ||
|
|
650f200a0b | ||
|
|
14d215bdf3 | ||
|
|
12aebc2fe9 | ||
|
|
f41b051ec7 | ||
|
|
25144eee89 | ||
|
|
0c62147536 | ||
|
|
7b282e21de | ||
|
|
c1fbe6d84c | ||
|
|
e20cbe4170 | ||
|
|
394955a03f | ||
|
|
16b4f5d065 | ||
|
|
c95295d8b4 | ||
|
|
658dce2607 | ||
|
|
01f8d0a27e | ||
|
|
39e1bfc84f | ||
|
|
e394416fa7 | ||
|
|
f1108bc0e2 | ||
|
|
ff31815d49 | ||
|
|
604aa63b47 | ||
|
|
ec4d036f50 | ||
|
|
1bf1c4ac63 | ||
|
|
981d46fbd4 | ||
|
|
eb6a353c31 | ||
|
|
ff673b1941 | ||
|
|
8b55cf8a3a | ||
|
|
d8682b4403 | ||
|
|
40a4bf195a | ||
|
|
312c5cbc3f | ||
|
|
f314c56ef0 | ||
|
|
ea8e5180ff | ||
|
|
7f76c3f2ce | ||
|
|
e4c5e99d0f | ||
|
|
80cfca5de2 | ||
|
|
9556a14de9 | ||
|
|
2025fc8325 | ||
|
|
2dd0f6a9ba | ||
|
|
a42039d6e5 | ||
|
|
7fafa8adfb | ||
|
|
f789657552 | ||
|
|
3e183bc10e | ||
|
|
16e5d93be3 | ||
|
|
6515e06a13 | ||
|
|
9ae0b32318 | ||
|
|
4cb3bc4185 | ||
|
|
f9ca24598e | ||
|
|
3ccc892d6a | ||
|
|
d7cacea843 | ||
|
|
e30233ac74 | ||
|
|
7c57631fcf | ||
|
|
0adc084dad | ||
|
|
e22199817c | ||
|
|
78f691d006 | ||
|
|
749bae1f16 | ||
|
|
1043e24322 | ||
|
|
a6d10b1fa7 | ||
|
|
a3159423f8 | ||
|
|
ecb5ca321b | ||
|
|
fd827fdfd8 | ||
|
|
92d77b14d5 | ||
|
|
be67d89d8b | ||
|
|
ddd1ce732a | ||
|
|
012d82183c | ||
|
|
f9c98ebcb3 | ||
|
|
10fe861dde | ||
|
|
16c027ecab | ||
|
|
cc578e7cc5 | ||
|
|
6972d3c4f9 | ||
|
|
46b164f2fb | ||
|
|
5431307527 | ||
|
|
79bf194ed6 | ||
|
|
506e670aa7 | ||
|
|
fdfe164dd1 | ||
|
|
af37056179 | ||
|
|
b0fc7187cf | ||
|
|
33ad1a7a86 | ||
|
|
dd72fb4ca5 | ||
|
|
e6c5f76872 | ||
|
|
14aa22d590 | ||
|
|
5ed4293641 | ||
|
|
f9fefcda57 | ||
|
|
99b40293db | ||
|
|
9b06e85f94 | ||
|
|
93d1d40ea5 | ||
|
|
98597c047a | ||
|
|
a5c1f4b0ee | ||
|
|
3d4c98d981 | ||
|
|
384e7dedb5 | ||
|
|
7df4453560 | ||
|
|
d406a1c341 | ||
|
|
6671d97b4a | ||
|
|
d02de72830 | ||
|
|
08f5172028 | ||
|
|
04f062547d | ||
|
|
4717d783dc | ||
|
|
93af064b36 | ||
|
|
c78d73d727 | ||
|
|
b69b3228be | ||
|
|
377f54700d | ||
|
|
d276339c80 | ||
|
|
b982a6a762 | ||
|
|
536fe637aa | ||
|
|
69f36eaa25 | ||
|
|
d6927a70bb | ||
|
|
1b1892a187 | ||
|
|
5bea71cd5c | ||
|
|
bb8d4e70ae | ||
|
|
8d8c7e8b7b | ||
|
|
9ff1f4d7b4 | ||
|
|
3a60dfe025 | ||
|
|
d358854e16 | ||
|
|
129587e94a | ||
|
|
0f575f4639 | ||
|
|
7aac741571 | ||
|
|
b8a9da8a4e | ||
|
|
be6b974334 | ||
|
|
194e81205b | ||
|
|
d711dcc99d | ||
|
|
3b72a12540 | ||
|
|
38a1b7765a | ||
|
|
02f2b8b6f0 | ||
|
|
7bfa75102c | ||
|
|
b0f3e7351c | ||
|
|
b5fa401db9 | ||
|
|
41f2710dea | ||
|
|
f19dda57f0 | ||
|
|
cc4a99ad80 | ||
|
|
1c1a3fc417 | ||
|
|
e97be5850b | ||
|
|
2c302654b7 | ||
|
|
3ee4dc77b2 | ||
|
|
db55f314c9 | ||
|
|
0b536b287f | ||
|
|
6f298a9917 | ||
|
|
70f829a2e5 | ||
|
|
1dfe07003f | ||
|
|
286b908062 | ||
|
|
4ec028e736 | ||
|
|
35a6d1437a | ||
|
|
7e3cb3de89 | ||
|
|
bd945039f1 | ||
|
|
46743c8f15 | ||
|
|
9b75284447 | ||
|
|
dca3b0052a | ||
|
|
6d05bb2de5 | ||
|
|
8b2e8d3804 | ||
|
|
1a2ab19ab4 | ||
|
|
04f010aa7b | ||
|
|
25323b4a3a | ||
|
|
f353c022f6 | ||
|
|
9d8b3d3428 | ||
|
|
e4b06772c0 | ||
|
|
a2b5c3ea08 | ||
|
|
f632e95600 | ||
|
|
4b5da8c6d4 | ||
|
|
77d7ffd10a | ||
|
|
d2f720011a | ||
|
|
6329a7711d | ||
|
|
fd14d044e3 | ||
|
|
6368d507e1 | ||
|
|
205b7f2401 | ||
|
|
f7d10f9530 | ||
|
|
66154bb51e | ||
|
|
fac4ad5313 | ||
|
|
fee63891ac | ||
|
|
efa979ee03 | ||
|
|
c756346357 | ||
|
|
cc95db25cc | ||
|
|
44970c3321 | ||
|
|
43c7934af7 | ||
|
|
3e7b51163c | ||
|
|
4d5f8a53f0 | ||
|
|
0b73ca8318 | ||
|
|
754cfc3bfd | ||
|
|
2ed1a5c06a | ||
|
|
b2073df3c3 | ||
|
|
6267edaa81 | ||
|
|
292f2de3e6 | ||
|
|
7803dc8be3 | ||
|
|
2a1260b79e | ||
|
|
f9ec438b7f | ||
|
|
de544c4856 | ||
|
|
55e04dd520 | ||
|
|
b2d06f900b | ||
|
|
b92ec71810 | ||
|
|
3a4199240e | ||
|
|
157de12bdf | ||
|
|
7c0c9fccdb | ||
|
|
2cb29d06b3 | ||
|
|
1fdeb50d93 | ||
|
|
f59632ae59 | ||
|
|
67b503da44 | ||
|
|
a351de6904 | ||
|
|
f94742abd5 | ||
|
|
891182637e | ||
|
|
086681c915 | ||
|
|
3970d2d02b | ||
|
|
2b3fddd015 | ||
|
|
30fa0480a3 | ||
|
|
320a81ce69 | ||
|
|
950411ef56 | ||
|
|
cb9cf084c0 | ||
|
|
bb18af09ae | ||
|
|
049bf0c7f9 | ||
|
|
ebc73ef775 | ||
|
|
1bd6f59355 | ||
|
|
c16396a690 | ||
|
|
32a9fe3e9c | ||
|
|
f48d133bc6 | ||
|
|
0678526d7d | ||
|
|
26d3e8371f | ||
|
|
6768f614c7 | ||
|
|
171dc84df1 | ||
|
|
5ca6513c04 | ||
|
|
41aeb0ac80 | ||
|
|
f871c4adef | ||
|
|
ad4d0e89b1 | ||
|
|
0d0c49caa1 | ||
|
|
7cbc7bdcfb | ||
|
|
6b46c8ed4a | ||
|
|
e33ffd1c8a | ||
|
|
8dce9cc938 | ||
|
|
f06218f8e3 | ||
|
|
5711eef2be | ||
|
|
db6aac603c | ||
|
|
f10e928106 | ||
|
|
99d963b99c | ||
|
|
5d35c22238 | ||
|
|
54fc11a235 | ||
|
|
ec99df3144 | ||
|
|
d722035883 | ||
|
|
52139fbaa0 | ||
|
|
88854eb558 | ||
|
|
c82bba01ee | ||
|
|
eaa33a03d7 | ||
|
|
89e8518f31 | ||
|
|
c7bf2e1da8 | ||
|
|
bf904a6afa | ||
|
|
43c14ae71b | ||
|
|
4abb8ef3c9 | ||
|
|
e46b92dd7d | ||
|
|
1a25faa5b9 | ||
|
|
3f4bf5f512 | ||
|
|
ae7dbf15ed | ||
|
|
b22320c48f | ||
|
|
61204e8d35 | ||
|
|
b0704c654c | ||
|
|
f40f8a8873 | ||
|
|
560a7db506 | ||
|
|
15baa8f70e | ||
|
|
97a7637294 | ||
|
|
3454656207 | ||
|
|
7dff6f26bc | ||
|
|
9e835a23fd | ||
|
|
c0259fb6ce | ||
|
|
acf106220b | ||
|
|
5d4dc9c907 | ||
|
|
2b9a56af7f | ||
|
|
f1e3ac65ac | ||
|
|
143f72cf6b | ||
|
|
5c8e49296c | ||
|
|
b2558b703c | ||
|
|
bf9d87106e | ||
|
|
5b7f507d9b | ||
|
|
59a7835ace | ||
|
|
2495838fe3 | ||
|
|
bab687bedf | ||
|
|
6467c3c8ee | ||
|
|
66c2b7aaa6 | ||
|
|
ab72c52661 | ||
|
|
f0bf1b8a54 | ||
|
|
119251719f | ||
|
|
0348400132 | ||
|
|
813473805b | ||
|
|
fe6368561b | ||
|
|
7ff06f424d | ||
|
|
31958592c7 | ||
|
|
adc21f4f75 | ||
|
|
9cecd89d6f | ||
|
|
d866b9b4d4 | ||
|
|
93f12baf51 | ||
|
|
78d6fd634b | ||
|
|
b5fc19f08a | ||
|
|
2dae6a6546 | ||
|
|
b6bba46391 | ||
|
|
b791d97116 | ||
|
|
6aa8255f34 | ||
|
|
98e2140761 | ||
|
|
a59e064778 | ||
|
|
655c4c66a7 | ||
|
|
407c128f65 | ||
|
|
59a261a5be | ||
|
|
ba4e1afefe | ||
|
|
98b53b81d8 | ||
|
|
8270cc0aa2 | ||
|
|
74207b1a87 | ||
|
|
77983445ce | ||
|
|
162190bcb8 | ||
|
|
86b92b20b7 | ||
|
|
2f5b60d548 | ||
|
|
c19661c977 | ||
|
|
82d101ca27 | ||
|
|
32f9c4e670 | ||
|
|
34c7225ab7 | ||
|
|
b02eb502e4 | ||
|
|
7d6b3dfd20 | ||
|
|
3ac39968d2 | ||
|
|
2b24ac54a0 | ||
|
|
6da6b794c7 | ||
|
|
ed689e27c9 | ||
|
|
186f9c3f18 | ||
|
|
42cdde3203 | ||
|
|
b9091702e9 | ||
|
|
d42ecbe74e | ||
|
|
e6c5446d63 | ||
|
|
cc277211b3 | ||
|
|
6703c15c52 | ||
|
|
4ecdaf573e | ||
|
|
71ec3e61be | ||
|
|
216fdb2393 | ||
|
|
c39acc6e3c | ||
|
|
d97b0478a7 | ||
|
|
d15d64eb67 | ||
|
|
3acd2656ee | ||
|
|
a4b003534a | ||
|
|
8752538a02 | ||
|
|
bde435753a | ||
|
|
d347e6fc5f | ||
|
|
27b01d3642 | ||
|
|
b78677e285 | ||
|
|
f8f6c74bcb | ||
|
|
7ce123939b | ||
|
|
e7b02cfb15 | ||
|
|
5931077b08 | ||
|
|
a521bdc0e3 | ||
|
|
80da565609 | ||
|
|
6bc46e4598 | ||
|
|
6562258db5 | ||
|
|
c219995218 | ||
|
|
62035431ed | ||
|
|
fa1fbca7dc | ||
|
|
391b7476b3 | ||
|
|
9c04ce665f | ||
|
|
dd87268197 | ||
|
|
8d9af59db2 | ||
|
|
1b754a35ff | ||
|
|
7230f91f61 | ||
|
|
93edfc315c | ||
|
|
74c8269531 | ||
|
|
acb6c0fc83 | ||
|
|
553d4cce93 | ||
|
|
ca81f144e6 | ||
|
|
ec84960347 | ||
|
|
38db0764af | ||
|
|
a647f63bb0 | ||
|
|
5b7087cc9e | ||
|
|
1a6fcd5da6 | ||
|
|
e31dd9f553 | ||
|
|
22844716d1 | ||
|
|
6abed76f70 | ||
|
|
72ff502e0f | ||
|
|
2b781df32b | ||
|
|
bf601c3906 | ||
|
|
b753e0ebb0 | ||
|
|
564211aceb | ||
|
|
538c759fef | ||
|
|
95edc34100 | ||
|
|
98682a2da9 | ||
|
|
33581fa61d | ||
|
|
b02e0aae98 | ||
|
|
6fa4e1cb6d |
213
.clang-format
213
.clang-format
@@ -1,105 +1,216 @@
|
||||
BasedOnStyle: Chromium
|
||||
Language: Cpp
|
||||
Standard: Cpp11
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -1
|
||||
AlignAfterOpenBracket: false
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignConsecutiveMacros: true
|
||||
AlignEscapedNewlines: true
|
||||
AlignOperands: false
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: DontAlign
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: true
|
||||
AllowShortIfStatementsOnASingleLine: true
|
||||
AllowShortLambdasOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
|
||||
AllowShortLoopsOnASingleLine: true
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: No
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BreakBeforeBraces: false
|
||||
BreakBeforeBinaryOperators: false
|
||||
BreakBeforeBraces: Stroustrup
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: true
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 0
|
||||
ColumnLimit: 0
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
QualifierAlignment: Leave
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 2
|
||||
Cpp11BracedListStyle: false
|
||||
DeriveLineEnding: true
|
||||
DerivePointerAlignment: true
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
PackConstructorInitializers: BinPack
|
||||
BasedOnStyle: ''
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
FixNamespaceComments: true
|
||||
IncludeBlocks: Preserve
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseLabels: true
|
||||
IndentCaseBlocks: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: AfterHash
|
||||
IndentWidth: 2
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 100
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 0
|
||||
PenaltyBreakBeforeFirstCallParameter: 0
|
||||
PenaltyBreakComment: 0
|
||||
PenaltyBreakFirstLessLess: 0
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 0
|
||||
PenaltyBreakTemplateDeclaration: 0
|
||||
PenaltyExcessCharacter: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 0
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: false
|
||||
PPIndentWidth: -1
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: false
|
||||
RemoveBracesLLVM: false
|
||||
RequiresClausePosition: OwnLine
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: false
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: true
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInAngles: Never
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterFunction: false
|
||||
AfterCaseLabel: false
|
||||
AfterStruct: false
|
||||
AfterClass: false
|
||||
AfterEnum: false
|
||||
AfterUnion: false
|
||||
AfterControlStatement: Never
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: true
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
BitFieldColonSpacing: Both
|
||||
Standard: Latest
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
WhitespaceSensitiveMacros:
|
||||
- STRINGIZE
|
||||
- PP_STRINGIZE
|
||||
- BOOST_PP_STRINGIZE
|
||||
- NS_SWIFT_NAME
|
||||
- CF_SWIFT_NAME
|
||||
...
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@@ -7,13 +7,9 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
For technical issues, questions and discussion please use the forum on https://forum.strawberrymusicplayer.org/
|
||||
Any issues related to feature requests will be closed. See the README for more details.
|
||||
|
||||
Check the Changelog to see if the issue is already fixed:
|
||||
https://github.com/strawberrymusicplayer/strawberry/blob/master/Changelog
|
||||
|
||||
If it's fixed, try the latest development build from: https://builds.strawberrymusicplayer.org/
|
||||
- [ ] I have checked the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ) for answers.
|
||||
- [ ] I have checked the [Changelog](https://github.com/strawberrymusicplayer/strawberry/blob/master/Changelog) that the issue is not already fixed.
|
||||
- [ ] I believe this issue is a bug, and not a general technical issue, question or feature requests that can be discussed on the [forum](https://forum.strawberrymusicplayer.org/).
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
1063
.github/workflows/build.yml
vendored
Normal file
1063
.github/workflows/build.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2180
.github/workflows/ccpp.yml
vendored
2180
.github/workflows/ccpp.yml
vendored
File diff suppressed because it is too large
Load Diff
88
.github/workflows/codeql.yml
vendored
Normal file
88
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: CodeQL
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: opensuse/tumbleweed
|
||||
|
||||
steps:
|
||||
|
||||
- name: Refresh repositories
|
||||
run: zypper -n --gpg-auto-import-keys ref
|
||||
|
||||
- name: Upgrade packages
|
||||
run: zypper -n --gpg-auto-import-keys dup
|
||||
|
||||
- name: Install packages
|
||||
run: >
|
||||
zypper -n --gpg-auto-import-keys in
|
||||
lsb-release
|
||||
rpm-build
|
||||
git
|
||||
tar
|
||||
gcc
|
||||
gcc-c++
|
||||
make
|
||||
cmake
|
||||
gettext-tools
|
||||
glibc-devel
|
||||
libboost_headers-devel
|
||||
boost-devel
|
||||
glib2-devel
|
||||
glib2-tools
|
||||
dbus-1-devel
|
||||
alsa-devel
|
||||
libnotify-devel
|
||||
libgnutls-devel
|
||||
protobuf-devel
|
||||
sqlite3-devel
|
||||
libpulse-devel
|
||||
gstreamer-devel
|
||||
gstreamer-plugins-base-devel
|
||||
vlc-devel
|
||||
taglib-devel
|
||||
libicu-devel
|
||||
libcdio-devel
|
||||
libgpod-devel
|
||||
libmtp-devel
|
||||
libchromaprint-devel
|
||||
qt6-core-devel
|
||||
qt6-gui-devel
|
||||
qt6-widgets-devel
|
||||
qt6-concurrent-devel
|
||||
qt6-network-devel
|
||||
qt6-sql-devel
|
||||
qt6-dbus-devel
|
||||
qt6-test-devel
|
||||
qt6-base-common-devel
|
||||
qt6-sql-sqlite
|
||||
qt6-linguist-devel
|
||||
desktop-file-utils
|
||||
update-desktop-files
|
||||
appstream-glib
|
||||
hicolor-icon-theme
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Add safe git directory
|
||||
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: cpp
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:cpp"
|
||||
39
.gitignore
vendored
39
.gitignore
vendored
@@ -65,19 +65,6 @@ ui_*.h
|
||||
*.moc
|
||||
*.qm
|
||||
|
||||
# QtCreator
|
||||
CMakeLists.txt.user*
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*creator.user*
|
||||
target_wrapper.*
|
||||
compile_commands.json
|
||||
|
||||
*.kdev4
|
||||
*.vscode
|
||||
*.code-workspace
|
||||
*.sublime-workspace
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
*.autosave
|
||||
@@ -111,15 +98,23 @@ translations.pot
|
||||
zanata.xml
|
||||
.zanata-cache/
|
||||
|
||||
# Snap
|
||||
parts/
|
||||
prime/
|
||||
stage/
|
||||
*.snap
|
||||
/snap/.snapcraft/
|
||||
/*_source.tar.bz2
|
||||
# QtCreator
|
||||
CMakeLists.txt.user*
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*creator.user*
|
||||
target_wrapper.*
|
||||
compile_commands.json
|
||||
|
||||
*.kdev4
|
||||
*.vscode
|
||||
*.code-workspace
|
||||
*.sublime-workspace
|
||||
|
||||
# MSVC
|
||||
CMakeSettings.json
|
||||
/.vs/
|
||||
/out/
|
||||
/.vs
|
||||
/out
|
||||
|
||||
# CLion
|
||||
/.idea
|
||||
|
||||
33
3rdparty/macdeployqt/shared.cpp
vendored
33
3rdparty/macdeployqt/shared.cpp
vendored
@@ -40,6 +40,8 @@
|
||||
#include <QDir>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QStack>
|
||||
#include <QDirIterator>
|
||||
#include <QLibraryInfo>
|
||||
@@ -187,9 +189,7 @@ OtoolInfo findDependencyInfo(const QString &binaryPath)
|
||||
return info;
|
||||
}
|
||||
|
||||
static const QRegularExpression regexp(QStringLiteral(
|
||||
"^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), "
|
||||
"current version (\\d+\\.\\d+\\.\\d+)(, weak)?\\)$"));
|
||||
static const QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak|, reexport)?\\)$"));
|
||||
|
||||
QString output = otool.readAllStandardOutput();
|
||||
QStringList outputLines = output.split("\n", Qt::SkipEmptyParts);
|
||||
@@ -220,6 +220,8 @@ OtoolInfo findDependencyInfo(const QString &binaryPath)
|
||||
for (const QString &outputLine : outputLines) {
|
||||
const auto match = regexp.match(outputLine);
|
||||
if (match.hasMatch()) {
|
||||
if (match.captured(1) == info.installName)
|
||||
continue; // Another arch reference to the same binary
|
||||
DylibInfo dylib;
|
||||
dylib.binaryPath = match.captured(1);
|
||||
dylib.compatibilityVersion = QVersionNumber::fromString(match.captured(2));
|
||||
@@ -300,11 +302,11 @@ FrameworkInfo parseOtoolLibraryLine(const QString &line, const QString &appBundl
|
||||
if (state == QtPath) {
|
||||
// Check for library name part
|
||||
if (part < parts.count() && parts.at(part).contains(".dylib")) {
|
||||
info.frameworkDirectory += "/" + (qtPath + currentPart + "/").simplified();
|
||||
info.frameworkDirectory += "/" + QString(qtPath + currentPart + "/").simplified();
|
||||
state = DylibName;
|
||||
continue;
|
||||
} else if (part < parts.count() && parts.at(part).endsWith(".framework")) {
|
||||
info.frameworkDirectory += "/" + (qtPath + "lib/").simplified();
|
||||
info.frameworkDirectory += "/" + QString(qtPath + "lib/").simplified();
|
||||
state = FrameworkName;
|
||||
continue;
|
||||
} else if (trimmed.startsWith("/") == false) { // If the line does not contain a full path, the app is using a binary Qt package.
|
||||
@@ -865,6 +867,18 @@ void changeInstallName(const QString &bundlePath, const FrameworkInfo &framework
|
||||
if (!framework.installName.isEmpty() && framework.installName != framework.sourceFilePath) {
|
||||
changeInstallName(framework.installName, deployedInstallName, binary);
|
||||
}
|
||||
// Workaround for the case when the library ID name is a symlink, while the dependencies
|
||||
// specified using the canonical path to the library (QTBUG-56814)
|
||||
QFileInfo fileInfo= QFileInfo(framework.installName);
|
||||
QString canonicalInstallName = fileInfo.canonicalFilePath();
|
||||
if (!canonicalInstallName.isEmpty() && canonicalInstallName != framework.installName) {
|
||||
changeInstallName(canonicalInstallName, deployedInstallName, binary);
|
||||
// some libraries' inner dependencies (such as ffmpeg, nettle) use symbol link (QTBUG-100093)
|
||||
QString innerDependency = fileInfo.canonicalPath() + "/" + fileInfo.fileName();
|
||||
if (innerDependency != canonicalInstallName && innerDependency != framework.installName) {
|
||||
changeInstallName(innerDependency, deployedInstallName, binary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1078,7 +1092,7 @@ QString getLibInfix(const QStringList &deployedFrameworks)
|
||||
{
|
||||
QString libInfix;
|
||||
for (const QString &framework : deployedFrameworks) {
|
||||
if (framework.startsWith(QStringLiteral("QtCore")) && framework.endsWith(QStringLiteral(".framework"))) {
|
||||
if (framework.startsWith(QStringLiteral("QtCore")) && framework.endsWith(QStringLiteral(".framework")) && !framework.contains(QStringLiteral("5Compat"))) {
|
||||
Q_ASSERT(framework.length() >= 16);
|
||||
// 16 == "QtCore" + ".framework"
|
||||
const int lengthOfLibInfix = framework.length() - 16;
|
||||
@@ -1301,6 +1315,7 @@ void deployPlugins(const ApplicationBundleInfo &appBundleInfo, const QString &pl
|
||||
<< "libgstfdkaac.dylib"
|
||||
<< "libgstflac.dylib"
|
||||
<< "libgstgio.dylib"
|
||||
//<< "libgstgme.dylib"
|
||||
<< "libgsthls.dylib"
|
||||
<< "libgsticydemux.dylib"
|
||||
<< "libgstid3demux.dylib"
|
||||
@@ -1473,9 +1488,9 @@ bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInf
|
||||
for (const QString &importPath : qmlImportPaths)
|
||||
argumentList << "-importPath" << importPath;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QString qmlImportsPath = QLibraryInfo::path(QLibraryInfo::Qml2ImportsPath);
|
||||
QString qmlImportsPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
|
||||
#else
|
||||
QString qmlImportsPath = QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath);
|
||||
QString qmlImportsPath = QLibraryInfo::location(QLibraryInfo::QmlImportsPath);
|
||||
#endif
|
||||
argumentList.append( "-importPath");
|
||||
argumentList.append(qmlImportsPath);
|
||||
@@ -1487,7 +1502,7 @@ bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInf
|
||||
LogError() << "Could not start qmlimpoortscanner. Process error is" << qmlImportScanner.errorString();
|
||||
return false;
|
||||
}
|
||||
qmlImportScanner.waitForFinished();
|
||||
qmlImportScanner.waitForFinished(-1);
|
||||
|
||||
// log qmlimportscanner errors
|
||||
qmlImportScanner.setReadChannel(QProcess::StandardError);
|
||||
|
||||
32
3rdparty/singleapplication/CMakeLists.txt
vendored
32
3rdparty/singleapplication/CMakeLists.txt
vendored
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
include(CheckIncludeFiles)
|
||||
include(CheckFunctionExists)
|
||||
@@ -6,31 +6,7 @@ include(CheckFunctionExists)
|
||||
check_function_exists(geteuid HAVE_GETEUID)
|
||||
check_function_exists(getpwuid HAVE_GETPWUID)
|
||||
|
||||
set(SINGLEAPP-SOURCES singleapplication.cpp singleapplication_p.cpp)
|
||||
set(SINGLEAPP-MOC-HEADERS singleapplication.h singleapplication_p.h)
|
||||
qt_wrap_cpp(SINGLEAPP-SOURCES-MOC ${SINGLEAPP-MOC-HEADERS})
|
||||
add_library(singleapplication STATIC ${SINGLEAPP-SOURCES} ${SINGLEAPP-SOURCES-MOC})
|
||||
target_include_directories(singleapplication PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
target_link_libraries(singleapplication PRIVATE
|
||||
${QtCore_LIBRARIES}
|
||||
${QtWidgets_LIBRARIES}
|
||||
${QtNetwork_LIBRARIES}
|
||||
)
|
||||
|
||||
set(SINGLECOREAPP-SOURCES singlecoreapplication.cpp singlecoreapplication_p.cpp)
|
||||
set(SINGLECOREAPP-MOC-HEADERS singlecoreapplication.h singlecoreapplication_p.h)
|
||||
qt_wrap_cpp(SINGLECOREAPP-SOURCES-MOC ${SINGLECOREAPP-MOC-HEADERS})
|
||||
add_library(singlecoreapplication STATIC ${SINGLECOREAPP-SOURCES} ${SINGLECOREAPP-SOURCES-MOC})
|
||||
target_include_directories(singlecoreapplication PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
target_link_libraries(singlecoreapplication PRIVATE
|
||||
${QtCore_LIBRARIES}
|
||||
${QtNetwork_LIBRARIES}
|
||||
)
|
||||
|
||||
configure_file(config.h.in "${CMAKE_CURRENT_BINARY_DIR}/config.h")
|
||||
|
||||
add_subdirectory(singleapplication)
|
||||
add_subdirectory(singlecoreapplication)
|
||||
|
||||
266
3rdparty/singleapplication/singleapplication.cpp
vendored
266
3rdparty/singleapplication/singleapplication.cpp
vendored
@@ -1,266 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QApplication>
|
||||
#include <QThread>
|
||||
#include <QSharedMemory>
|
||||
#include <QLocalSocket>
|
||||
#include <QByteArray>
|
||||
#include <QElapsedTimer>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
/**
|
||||
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
|
||||
* @param argc
|
||||
* @param argv
|
||||
* @param allowSecondary Whether to enable secondary instance support
|
||||
* @param options Optional flags to toggle specific behaviour
|
||||
* @param timeout Maximum time blocking functions are allowed during app load
|
||||
*/
|
||||
SingleApplication::SingleApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
|
||||
: app_t(argc, argv),
|
||||
d_ptr(new SingleApplicationPrivate(this)) {
|
||||
|
||||
Q_D(SingleApplication);
|
||||
|
||||
// Store the current mode of the program
|
||||
d->options_ = options;
|
||||
|
||||
// Generating an application ID used for identifying the shared memory block and QLocalServer
|
||||
d->genBlockServerName();
|
||||
|
||||
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
|
||||
SingleApplicationPrivate::randomSleep();
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
|
||||
d->memory_ = new QSharedMemory(d->blockServerName_);
|
||||
d->memory_->attach();
|
||||
delete d->memory_;
|
||||
#endif
|
||||
|
||||
// Guarantee thread safe behaviour with a shared memory block.
|
||||
d->memory_ = new QSharedMemory(d->blockServerName_);
|
||||
|
||||
// Create a shared memory block
|
||||
if (d->memory_->create(sizeof(InstancesInfo))) {
|
||||
// Initialize the shared memory block
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleApplication: Unable to lock memory block after create.";
|
||||
abortSafely();
|
||||
}
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
else {
|
||||
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
|
||||
// Attempt to attach to the memory segment
|
||||
if (!d->memory_->attach()) {
|
||||
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
|
||||
abortSafely();
|
||||
}
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qCritical() << "SingleApplication: Unable to create block.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Make sure the shared memory block is initialised and in consistent state
|
||||
forever {
|
||||
// If the shared memory block's checksum is valid continue
|
||||
if (d->blockChecksum() == instance->checksum) break;
|
||||
|
||||
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
|
||||
if (time.elapsed() > 5000) {
|
||||
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
|
||||
// Otherwise wait for a random period and try again.
|
||||
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
SingleApplicationPrivate::randomSleep();
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
if (!instance->primary) {
|
||||
d->startPrimary();
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if another instance can be started
|
||||
if (allowSecondary) {
|
||||
d->startSecondary();
|
||||
if (d->options_ & Mode::SecondaryNotification) {
|
||||
d->connectToPrimary(timeout, SingleApplicationPrivate::SecondaryInstance);
|
||||
}
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
|
||||
d->connectToPrimary(timeout, SingleApplicationPrivate::NewInstance);
|
||||
|
||||
delete d;
|
||||
|
||||
::exit(EXIT_SUCCESS);
|
||||
|
||||
}
|
||||
|
||||
SingleApplication::~SingleApplication() {
|
||||
Q_D(SingleApplication);
|
||||
delete d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is primary.
|
||||
* @return Returns true if the instance is primary, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::isPrimary() const {
|
||||
Q_D(const SingleApplication);
|
||||
return d->server_ != nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is secondary.
|
||||
* @return Returns true if the instance is secondary, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::isSecondary() const {
|
||||
Q_D(const SingleApplication);
|
||||
return d->server_ == nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to identify an instance by returning unique consecutive instance ids.
|
||||
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
|
||||
* @return Returns a unique instance id.
|
||||
*/
|
||||
quint32 SingleApplication::instanceId() const {
|
||||
Q_D(const SingleApplication);
|
||||
return d->instanceNumber_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OS PID (Process Identifier) of the process running the primary instance.
|
||||
* Especially useful when SingleApplication is coupled with OS. specific APIs.
|
||||
* @return Returns the primary instance PID.
|
||||
*/
|
||||
qint64 SingleApplication::primaryPid() const {
|
||||
Q_D(const SingleApplication);
|
||||
return d->primaryPid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the primary instance is running as.
|
||||
* @return Returns the username the primary instance is running as.
|
||||
*/
|
||||
QString SingleApplication::primaryUser() const {
|
||||
Q_D(const SingleApplication);
|
||||
return d->primaryUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the current instance is running as.
|
||||
* @return Returns the username the current instance is running as.
|
||||
*/
|
||||
QString SingleApplication::currentUser() const {
|
||||
return SingleApplicationPrivate::getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the Primary Instance.
|
||||
* @param message The message to send.
|
||||
* @param timeout the maximum timeout in milliseconds for blocking functions.
|
||||
* @return true if the message was sent successfully, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::sendMessage(const QByteArray &message, const int timeout) {
|
||||
|
||||
Q_D(SingleApplication);
|
||||
|
||||
// Nobody to connect to
|
||||
if (isPrimary()) return false;
|
||||
|
||||
// Make sure the socket is connected
|
||||
if (!d->connectToPrimary(timeout, SingleApplicationPrivate::Reconnect)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return d->writeConfirmedMessage(timeout, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the shared memory block and exits with a failure.
|
||||
* This function halts program execution.
|
||||
*/
|
||||
void SingleApplication::abortSafely() {
|
||||
|
||||
Q_D(SingleApplication);
|
||||
|
||||
qCritical() << "SingleApplication: " << d->memory_->error() << d->memory_->errorString();
|
||||
delete d;
|
||||
|
||||
::exit(EXIT_FAILURE);
|
||||
|
||||
}
|
||||
18
3rdparty/singleapplication/singleapplication/CMakeLists.txt
vendored
Normal file
18
3rdparty/singleapplication/singleapplication/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
add_definitions(-DSINGLEAPPLICATION)
|
||||
|
||||
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
|
||||
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
add_library(singleapplication STATIC ${SOURCES} ${MOC})
|
||||
target_include_directories(singleapplication PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
||||
${Boost_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(singleapplication PUBLIC
|
||||
${QtCore_LIBRARIES}
|
||||
${QtWidgets_LIBRARIES}
|
||||
${QtNetwork_LIBRARIES}
|
||||
)
|
||||
13
3rdparty/singleapplication/singleapplication/singleapplication.h
vendored
Normal file
13
3rdparty/singleapplication/singleapplication/singleapplication.h
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef SINGLEAPPLICATION_H
|
||||
#define SINGLEAPPLICATION_H
|
||||
|
||||
#ifdef SINGLEAPPLICATION
|
||||
# error "SINGLEAPPLICATION already defined."
|
||||
#endif
|
||||
|
||||
#define SINGLEAPPLICATION
|
||||
#include "../singleapplication_t.h"
|
||||
#undef SINGLEAPPLICATION_T_H
|
||||
#undef SINGLEAPPLICATION
|
||||
|
||||
#endif // SINGLEAPPLICATION_H
|
||||
114
3rdparty/singleapplication/singleapplication_p.cpp
vendored
114
3rdparty/singleapplication/singleapplication_p.cpp
vendored
@@ -68,44 +68,41 @@
|
||||
# include <QDateTime>
|
||||
#endif
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_t.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *ptr)
|
||||
SingleApplicationPrivateClass::SingleApplicationPrivateClass(SingleApplicationClass *ptr)
|
||||
: q_ptr(ptr),
|
||||
memory_(nullptr),
|
||||
socket_(nullptr),
|
||||
server_(nullptr),
|
||||
instanceNumber_(-1) {}
|
||||
|
||||
SingleApplicationPrivate::~SingleApplicationPrivate() {
|
||||
SingleApplicationPrivateClass::~SingleApplicationPrivateClass() {
|
||||
|
||||
if (socket_ != nullptr) {
|
||||
if (socket_ != nullptr && socket_->isOpen()) {
|
||||
socket_->close();
|
||||
delete socket_;
|
||||
socket_ = nullptr;
|
||||
}
|
||||
|
||||
if (memory_ != nullptr) {
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
if (server_ != nullptr) {
|
||||
server_->close();
|
||||
delete server_;
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
instance->primary = false;
|
||||
instance->primaryPid = -1;
|
||||
instance->primaryUser[0] = '\0';
|
||||
instance->checksum = blockChecksum();
|
||||
}
|
||||
memory_->unlock();
|
||||
|
||||
delete memory_;
|
||||
memory_ = nullptr;
|
||||
if (memory_->isAttached()) {
|
||||
memory_->detach();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString SingleApplicationPrivate::getUsername() {
|
||||
QString SingleApplicationPrivateClass::getUsername() {
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
QString username;
|
||||
@@ -141,36 +138,36 @@ QString SingleApplicationPrivate::getUsername() {
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::genBlockServerName() {
|
||||
void SingleApplicationPrivateClass::genBlockServerName() {
|
||||
|
||||
QCryptographicHash appData(QCryptographicHash::Sha256);
|
||||
appData.addData("SingleApplication");
|
||||
appData.addData(SingleApplication::app_t::applicationName().toUtf8());
|
||||
appData.addData(SingleApplication::app_t::organizationName().toUtf8());
|
||||
appData.addData(SingleApplication::app_t::organizationDomain().toUtf8());
|
||||
appData.addData(SingleApplicationClass::applicationName().toUtf8());
|
||||
appData.addData(SingleApplicationClass::organizationName().toUtf8());
|
||||
appData.addData(SingleApplicationClass::organizationDomain().toUtf8());
|
||||
|
||||
if (!(options_ & SingleApplication::Mode::ExcludeAppVersion)) {
|
||||
appData.addData(SingleApplication::app_t::applicationVersion().toUtf8());
|
||||
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppVersion)) {
|
||||
appData.addData(SingleApplicationClass::applicationVersion().toUtf8());
|
||||
}
|
||||
|
||||
if (!(options_ & SingleApplication::Mode::ExcludeAppPath)) {
|
||||
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppPath)) {
|
||||
#if defined(Q_OS_UNIX)
|
||||
const QByteArray appImagePath = qgetenv("APPIMAGE");
|
||||
if (appImagePath.isEmpty()) {
|
||||
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
|
||||
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
|
||||
}
|
||||
else {
|
||||
appData.addData(appImagePath);
|
||||
};
|
||||
}
|
||||
#elif defined(Q_OS_WIN)
|
||||
appData.addData(SingleApplication::app_t::applicationFilePath().toLower().toUtf8());
|
||||
appData.addData(SingleApplicationClass::applicationFilePath().toLower().toUtf8());
|
||||
#else
|
||||
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
|
||||
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
|
||||
#endif
|
||||
}
|
||||
|
||||
// User level block requires a user specific data in the hash
|
||||
if (options_ & SingleApplication::Mode::User) {
|
||||
if (options_ & SingleApplicationClass::Mode::User) {
|
||||
appData.addData(getUsername().toUtf8());
|
||||
}
|
||||
|
||||
@@ -179,7 +176,7 @@ void SingleApplicationPrivate::genBlockServerName() {
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::initializeMemoryBlock() const {
|
||||
void SingleApplicationPrivateClass::initializeMemoryBlock() const {
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
instance->primary = false;
|
||||
@@ -190,7 +187,7 @@ void SingleApplicationPrivate::initializeMemoryBlock() const {
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::startPrimary() {
|
||||
void SingleApplicationPrivateClass::startPrimary() {
|
||||
|
||||
// Reset the number of connections
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
@@ -203,10 +200,10 @@ void SingleApplicationPrivate::startPrimary() {
|
||||
// Successful creation means that no main process exists
|
||||
// So we start a QLocalServer to listen for connections
|
||||
QLocalServer::removeServer(blockServerName_);
|
||||
server_ = new QLocalServer();
|
||||
server_ = new QLocalServer(this);
|
||||
|
||||
// Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions
|
||||
if (options_ & SingleApplication::Mode::User) {
|
||||
if (options_ & SingleApplicationClass::Mode::User) {
|
||||
server_->setSocketOptions(QLocalServer::UserAccessOption);
|
||||
}
|
||||
else {
|
||||
@@ -214,11 +211,11 @@ void SingleApplicationPrivate::startPrimary() {
|
||||
}
|
||||
|
||||
server_->listen(blockServerName_);
|
||||
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivate::slotConnectionEstablished);
|
||||
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivateClass::slotConnectionEstablished);
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::startSecondary() {
|
||||
void SingleApplicationPrivateClass::startSecondary() {
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
|
||||
@@ -228,18 +225,18 @@ void SingleApplicationPrivate::startSecondary() {
|
||||
|
||||
}
|
||||
|
||||
bool SingleApplicationPrivate::connectToPrimary(const int timeout, const ConnectionType connectionType) {
|
||||
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) {
|
||||
|
||||
// Connect to the Local Server of the Primary Instance if not already connected.
|
||||
if (socket_ == nullptr) {
|
||||
socket_ = new QLocalSocket();
|
||||
socket_ = new QLocalSocket(this);
|
||||
}
|
||||
|
||||
if (socket_->state() == QLocalSocket::ConnectedState) return true;
|
||||
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
if (socket_->state() != QLocalSocket::ConnectedState) {
|
||||
|
||||
forever {
|
||||
@@ -261,7 +258,7 @@ bool SingleApplicationPrivate::connectToPrimary(const int timeout, const Connect
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation message according to the SingleApplication protocol
|
||||
// Initialization message according to the SingleApplication protocol
|
||||
QByteArray initMsg;
|
||||
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
||||
writeStream.setVersion(QDataStream::Qt_5_8);
|
||||
@@ -282,11 +279,11 @@ bool SingleApplicationPrivate::connectToPrimary(const int timeout, const Connect
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::writeAck(QLocalSocket *sock) {
|
||||
void SingleApplicationPrivateClass::writeAck(QLocalSocket *sock) {
|
||||
sock->putChar('\n');
|
||||
}
|
||||
|
||||
bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
|
||||
bool SingleApplicationPrivateClass::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
|
||||
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
@@ -306,7 +303,7 @@ bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QB
|
||||
|
||||
}
|
||||
|
||||
bool SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
|
||||
bool SingleApplicationPrivateClass::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
|
||||
|
||||
socket_->write(msg);
|
||||
socket_->flush();
|
||||
@@ -321,7 +318,7 @@ bool SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByt
|
||||
|
||||
}
|
||||
|
||||
quint16 SingleApplicationPrivate::blockChecksum() const {
|
||||
quint16 SingleApplicationPrivateClass::blockChecksum() const {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
|
||||
@@ -333,7 +330,7 @@ quint16 SingleApplicationPrivate::blockChecksum() const {
|
||||
|
||||
}
|
||||
|
||||
qint64 SingleApplicationPrivate::primaryPid() const {
|
||||
qint64 SingleApplicationPrivateClass::primaryPid() const {
|
||||
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
@@ -344,7 +341,7 @@ qint64 SingleApplicationPrivate::primaryPid() const {
|
||||
|
||||
}
|
||||
|
||||
QString SingleApplicationPrivate::primaryUser() const {
|
||||
QString SingleApplicationPrivateClass::primaryUser() const {
|
||||
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
@@ -358,23 +355,23 @@ QString SingleApplicationPrivate::primaryUser() const {
|
||||
/**
|
||||
* @brief Executed when a connection has been made to the LocalServer
|
||||
*/
|
||||
void SingleApplicationPrivate::slotConnectionEstablished() {
|
||||
void SingleApplicationPrivateClass::slotConnectionEstablished() {
|
||||
|
||||
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
|
||||
connectionMap_.insert(nextConnSocket, ConnectionInfo());
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [this, nextConnSocket]() {
|
||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
||||
slotClientConnectionClosed(nextConnSocket, info.instanceId);
|
||||
});
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [this, nextConnSocket]() {
|
||||
connectionMap_.remove(nextConnSocket);
|
||||
});
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [this, nextConnSocket]() {
|
||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
||||
switch (info.stage) {
|
||||
case StageInitHeader:
|
||||
@@ -387,16 +384,16 @@ void SingleApplicationPrivate::slotConnectionEstablished() {
|
||||
readMessageHeader(nextConnSocket, StageConnectedBody);
|
||||
break;
|
||||
case StageConnectedBody:
|
||||
this->slotDataAvailable(nextConnSocket, info.instanceId);
|
||||
slotDataAvailable(nextConnSocket, info.instanceId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivate::ConnectionStage nextStage) {
|
||||
void SingleApplicationPrivateClass::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivateClass::ConnectionStage nextStage) {
|
||||
|
||||
if (!connectionMap_.contains(sock)) {
|
||||
return;
|
||||
@@ -420,7 +417,7 @@ void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const Singl
|
||||
|
||||
}
|
||||
|
||||
bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
|
||||
bool SingleApplicationPrivateClass::isFrameComplete(QLocalSocket *sock) {
|
||||
|
||||
if (!connectionMap_.contains(sock)) {
|
||||
return false;
|
||||
@@ -431,9 +428,9 @@ bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
|
||||
void SingleApplicationPrivateClass::readInitMessageBody(QLocalSocket *sock) {
|
||||
|
||||
Q_Q(SingleApplication);
|
||||
Q_Q(SingleApplicationClass);
|
||||
|
||||
if (!isFrameComplete(sock)) {
|
||||
return;
|
||||
@@ -449,10 +446,9 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
|
||||
readStream >> latin1Name;
|
||||
|
||||
// connection type
|
||||
ConnectionType connectionType = InvalidConnection;
|
||||
quint8 connTypeVal = InvalidConnection;
|
||||
readStream >> connTypeVal;
|
||||
connectionType = static_cast<ConnectionType>(connTypeVal);
|
||||
const ConnectionType connectionType = static_cast<ConnectionType>(connTypeVal);
|
||||
|
||||
// instance id
|
||||
quint32 instanceId = 0;
|
||||
@@ -479,7 +475,7 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
|
||||
info.instanceId = instanceId;
|
||||
info.stage = StageConnectedHeader;
|
||||
|
||||
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplication::Mode::SecondaryNotification)) {
|
||||
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplicationClass::Mode::SecondaryNotification)) {
|
||||
emit q->instanceStarted();
|
||||
}
|
||||
|
||||
@@ -487,9 +483,9 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
|
||||
void SingleApplicationPrivateClass::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
|
||||
|
||||
Q_Q(SingleApplication);
|
||||
Q_Q(SingleApplicationClass);
|
||||
|
||||
if (!isFrameComplete(dataSocket)) {
|
||||
return;
|
||||
@@ -506,7 +502,7 @@ void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
|
||||
void SingleApplicationPrivateClass::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
|
||||
|
||||
if (closedSocket->bytesAvailable() > 0) {
|
||||
slotDataAvailable(closedSocket, instanceId);
|
||||
@@ -514,7 +510,7 @@ void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSo
|
||||
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::randomSleep() {
|
||||
void SingleApplicationPrivateClass::randomSleep() {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
|
||||
|
||||
47
3rdparty/singleapplication/singleapplication_p.h
vendored
47
3rdparty/singleapplication/singleapplication_p.h
vendored
@@ -39,31 +39,19 @@
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_t.h"
|
||||
|
||||
class QLocalServer;
|
||||
class QLocalSocket;
|
||||
class QSharedMemory;
|
||||
|
||||
struct InstancesInfo {
|
||||
bool primary;
|
||||
quint32 secondary;
|
||||
qint64 primaryPid;
|
||||
char primaryUser[128];
|
||||
quint16 checksum;
|
||||
};
|
||||
|
||||
struct ConnectionInfo {
|
||||
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
|
||||
quint64 msgLen;
|
||||
quint32 instanceId;
|
||||
quint8 stage;
|
||||
};
|
||||
|
||||
class SingleApplicationPrivate : public QObject {
|
||||
class SingleApplicationPrivateClass : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr);
|
||||
~SingleApplicationPrivateClass() override;
|
||||
|
||||
enum ConnectionType : quint8 {
|
||||
InvalidConnection = 0,
|
||||
NewInstance = 1,
|
||||
@@ -74,12 +62,25 @@ class SingleApplicationPrivate : public QObject {
|
||||
StageInitHeader = 0,
|
||||
StageInitBody = 1,
|
||||
StageConnectedHeader = 2,
|
||||
StageConnectedBody = 3,
|
||||
StageConnectedBody = 3
|
||||
};
|
||||
Q_DECLARE_PUBLIC(SingleApplication)
|
||||
Q_DECLARE_PUBLIC(SingleApplicationClass)
|
||||
|
||||
explicit SingleApplicationPrivate(SingleApplication *ptr);
|
||||
~SingleApplicationPrivate() override;
|
||||
struct InstancesInfo {
|
||||
explicit InstancesInfo() : primary(false), secondary(0), primaryPid(0), checksum(0) {}
|
||||
bool primary;
|
||||
quint32 secondary;
|
||||
qint64 primaryPid;
|
||||
char primaryUser[128];
|
||||
quint16 checksum;
|
||||
};
|
||||
|
||||
struct ConnectionInfo {
|
||||
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
|
||||
quint64 msgLen;
|
||||
quint32 instanceId;
|
||||
quint8 stage;
|
||||
};
|
||||
|
||||
static QString getUsername();
|
||||
void genBlockServerName();
|
||||
@@ -98,13 +99,13 @@ class SingleApplicationPrivate : public QObject {
|
||||
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
|
||||
static void randomSleep();
|
||||
|
||||
SingleApplication *q_ptr;
|
||||
SingleApplicationClass *q_ptr;
|
||||
QSharedMemory *memory_;
|
||||
QLocalSocket *socket_;
|
||||
QLocalServer *server_;
|
||||
quint32 instanceNumber_;
|
||||
QString blockServerName_;
|
||||
SingleApplication::Options options_;
|
||||
SingleApplicationClass::Options options_;
|
||||
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
|
||||
|
||||
public slots:
|
||||
|
||||
330
3rdparty/singleapplication/singleapplication_t.cpp
vendored
Normal file
330
3rdparty/singleapplication/singleapplication_t.cpp
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include <boost/scope_exit.hpp>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QThread>
|
||||
#include <QSharedMemory>
|
||||
#include <QLocalSocket>
|
||||
#include <QByteArray>
|
||||
#include <QElapsedTimer>
|
||||
#include <QtDebug>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
# include <QNativeIpcKey>
|
||||
#endif
|
||||
|
||||
#include "singleapplication_t.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
/**
|
||||
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
|
||||
* @param argc
|
||||
* @param argv
|
||||
* @param allowSecondary Whether to enable secondary instance support
|
||||
* @param options Optional flags to toggle specific behaviour
|
||||
* @param timeout Maximum time blocking functions are allowed during app load
|
||||
*/
|
||||
SingleApplicationClass::SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
|
||||
: ApplicationClass(argc, argv),
|
||||
d_ptr(new SingleApplicationPrivateClass(this)) {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
// Store the current mode of the program
|
||||
d->options_ = options;
|
||||
|
||||
// Generating an application ID used for identifying the shared memory block and QLocalServer
|
||||
d->genBlockServerName();
|
||||
|
||||
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
|
||||
SingleApplicationPrivateClass::randomSleep();
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
|
||||
{
|
||||
# if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(QNativeIpcKey(d->blockServerName_));
|
||||
# else
|
||||
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(d->blockServerName_);
|
||||
# endif
|
||||
if (memory->attach()) {
|
||||
memory->detach();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Guarantee thread safe behaviour with a shared memory block.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
QSharedMemory *memory = new QSharedMemory(QNativeIpcKey(d->blockServerName_), this);
|
||||
#else
|
||||
QSharedMemory *memory = new QSharedMemory(d->blockServerName_, this);
|
||||
#endif
|
||||
d->memory_ = memory;
|
||||
|
||||
bool primary = false;
|
||||
|
||||
// Create a shared memory block
|
||||
if (d->memory_->create(sizeof(SingleApplicationPrivateClass::InstancesInfo))) {
|
||||
primary = true;
|
||||
}
|
||||
else if (d->memory_->error() == QSharedMemory::AlreadyExists) {
|
||||
if (!d->memory_->attach()) {
|
||||
qCritical() << "SingleApplication: Unable to attach to shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
qCritical() << "SingleApplication: Unable to create shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
bool locked = false;
|
||||
|
||||
BOOST_SCOPE_EXIT((memory)(&locked)) {
|
||||
if (locked && !memory->unlock()) {
|
||||
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
|
||||
return;
|
||||
}
|
||||
}BOOST_SCOPE_EXIT_END
|
||||
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleApplication: Unable to lock shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
||||
return;
|
||||
}
|
||||
locked = true;
|
||||
|
||||
if (primary) {
|
||||
// Initialize the shared memory block
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
|
||||
SingleApplicationPrivateClass::InstancesInfo *instance = static_cast<SingleApplicationPrivateClass::InstancesInfo*>(d->memory_->data());
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Make sure the shared memory block is initialized and in a consistent state
|
||||
while (d->blockChecksum() != instance->checksum) {
|
||||
|
||||
// If more than 5 seconds have elapsed, assume the primary instance crashed and assume its position
|
||||
if (time.elapsed() > 5000) {
|
||||
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5 seconds. Assuming primary instance failure.";
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
|
||||
// Otherwise wait for a random period and try again.
|
||||
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialize faster
|
||||
if (locked) {
|
||||
if (d->memory_->unlock()) {
|
||||
locked = false;
|
||||
}
|
||||
else {
|
||||
qCritical() << "SingleApplication: Unable to unlock shared memory block for random wait:" << memory->error() << memory->errorString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SingleApplicationPrivateClass::randomSleep();
|
||||
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleApplication: Unable to lock shared memory block after random wait:" << memory->error() << memory->errorString();
|
||||
return;
|
||||
}
|
||||
locked = true;
|
||||
|
||||
}
|
||||
|
||||
if (instance->primary) {
|
||||
// Check if another instance can be started
|
||||
if (allowSecondary) {
|
||||
d->startSecondary();
|
||||
if (d->options_ & Mode::SecondaryNotification) {
|
||||
d->connectToPrimary(timeout, SingleApplicationPrivateClass::SecondaryInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
d->startPrimary();
|
||||
primary = true;
|
||||
}
|
||||
|
||||
if (locked) {
|
||||
if (d->memory_->unlock()) {
|
||||
locked = false;
|
||||
}
|
||||
else {
|
||||
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!primary && !allowSecondary) {
|
||||
d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SingleApplicationClass::~SingleApplicationClass() {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
delete d;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is primary.
|
||||
* @return Returns true if the instance is primary, false otherwise.
|
||||
*/
|
||||
bool SingleApplicationClass::isPrimary() const {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(const SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(const SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
return d->server_ != nullptr;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is secondary.
|
||||
* @return Returns true if the instance is secondary, false otherwise.
|
||||
*/
|
||||
bool SingleApplicationClass::isSecondary() const {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(const SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(const SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
return d->server_ == nullptr;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to identify an instance by returning unique consecutive instance ids.
|
||||
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
|
||||
* @return Returns a unique instance id.
|
||||
*/
|
||||
quint32 SingleApplicationClass::instanceId() const {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(const SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(const SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
return d->instanceNumber_;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OS PID (Process Identifier) of the process running the primary instance.
|
||||
* Especially useful when SingleApplication is coupled with OS. specific APIs.
|
||||
* @return Returns the primary instance PID.
|
||||
*/
|
||||
qint64 SingleApplicationClass::primaryPid() const {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(const SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(const SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
return d->primaryPid();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the primary instance is running as.
|
||||
* @return Returns the username the primary instance is running as.
|
||||
*/
|
||||
QString SingleApplicationClass::primaryUser() const {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(const SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(const SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
return d->primaryUser();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the current instance is running as.
|
||||
* @return Returns the username the current instance is running as.
|
||||
*/
|
||||
QString SingleApplicationClass::currentUser() const {
|
||||
return SingleApplicationPrivateClass::getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the Primary Instance.
|
||||
* @param message The message to send.
|
||||
* @param timeout the maximum timeout in milliseconds for blocking functions.
|
||||
* @return true if the message was sent successfully, false otherwise.
|
||||
*/
|
||||
bool SingleApplicationClass::sendMessage(const QByteArray &message, const int timeout) {
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_D(SingleApplication);
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_D(SingleCoreApplication);
|
||||
#endif
|
||||
|
||||
// Nobody to connect to
|
||||
if (isPrimary()) return false;
|
||||
|
||||
// Make sure the socket is connected
|
||||
if (!d->connectToPrimary(timeout, SingleApplicationPrivateClass::Reconnect)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return d->writeConfirmedMessage(timeout, message);
|
||||
|
||||
}
|
||||
@@ -31,25 +31,41 @@
|
||||
//
|
||||
//
|
||||
|
||||
#ifndef SINGLEAPPLICATION_H
|
||||
#define SINGLEAPPLICATION_H
|
||||
#ifndef SINGLEAPPLICATION_T_H
|
||||
#define SINGLEAPPLICATION_T_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QApplication>
|
||||
|
||||
#undef ApplicationClass
|
||||
#undef SingleApplicationClass
|
||||
#undef SingleApplicationPrivateClass
|
||||
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
# include <QApplication>
|
||||
# define ApplicationClass QApplication
|
||||
# define SingleApplicationClass SingleApplication
|
||||
# define SingleApplicationPrivateClass SingleApplicationPrivate
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
# include <QCoreApplication>
|
||||
# define ApplicationClass QCoreApplication
|
||||
# define SingleApplicationClass SingleCoreApplication
|
||||
# define SingleApplicationPrivateClass SingleCoreApplicationPrivate
|
||||
#else
|
||||
# error "Define SINGLEAPPLICATION or SINGLECOREAPPLICATION."
|
||||
#endif
|
||||
|
||||
#include <QFlags>
|
||||
#include <QByteArray>
|
||||
|
||||
class SingleApplicationPrivate;
|
||||
class SingleApplicationPrivateClass;
|
||||
|
||||
/**
|
||||
* @brief The SingleApplication class handles multiple instances of the same Application
|
||||
* @see QApplication
|
||||
*/
|
||||
class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-parent-argument
|
||||
class SingleApplicationClass : public ApplicationClass { // clazy:exclude=ctor-missing-parent-argument
|
||||
Q_OBJECT
|
||||
|
||||
using app_t = QApplication;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Mode of operation of SingleApplication.
|
||||
@@ -61,7 +77,7 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
|
||||
* block will be user wide.
|
||||
* @enum
|
||||
*/
|
||||
enum Mode {
|
||||
enum class Mode {
|
||||
User = 1 << 0,
|
||||
System = 1 << 1,
|
||||
SecondaryNotification = 1 << 2,
|
||||
@@ -86,11 +102,11 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
|
||||
* instance and the secondary instance.
|
||||
* @note The timeout is just a hint for the maximum time of blocking
|
||||
* operations. It does not guarantee that the SingleApplication
|
||||
* initialisation will be completed in given time, though is a good hint.
|
||||
* initialization will be completed in given time, though is a good hint.
|
||||
* Usually 4*timeout would be the worst case (fail) scenario.
|
||||
*/
|
||||
explicit SingleApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
|
||||
~SingleApplication() override;
|
||||
explicit SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
|
||||
~SingleApplicationClass() override;
|
||||
|
||||
/**
|
||||
* @brief Returns if the instance is the primary instance
|
||||
@@ -142,11 +158,15 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
|
||||
void receivedMessage(quint32 instanceId, QByteArray message);
|
||||
|
||||
private:
|
||||
SingleApplicationPrivate *d_ptr;
|
||||
SingleApplicationPrivateClass *d_ptr;
|
||||
#if defined(SINGLEAPPLICATION)
|
||||
Q_DECLARE_PRIVATE(SingleApplication)
|
||||
#elif defined(SINGLECOREAPPLICATION)
|
||||
Q_DECLARE_PRIVATE(SingleCoreApplication)
|
||||
#endif
|
||||
void abortSafely();
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplicationClass::Options)
|
||||
|
||||
#endif // SINGLEAPPLICATION_H
|
||||
#endif // SINGLEAPPLICATION_T_H
|
||||
266
3rdparty/singleapplication/singlecoreapplication.cpp
vendored
266
3rdparty/singleapplication/singlecoreapplication.cpp
vendored
@@ -1,266 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QSharedMemory>
|
||||
#include <QLocalSocket>
|
||||
#include <QByteArray>
|
||||
#include <QElapsedTimer>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "singlecoreapplication.h"
|
||||
#include "singlecoreapplication_p.h"
|
||||
|
||||
/**
|
||||
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
|
||||
* @param argc
|
||||
* @param argv
|
||||
* @param allowSecondary Whether to enable secondary instance support
|
||||
* @param options Optional flags to toggle specific behaviour
|
||||
* @param timeout Maximum time blocking functions are allowed during app load
|
||||
*/
|
||||
SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
|
||||
: app_t(argc, argv),
|
||||
d_ptr(new SingleCoreApplicationPrivate(this)) {
|
||||
|
||||
Q_D(SingleCoreApplication);
|
||||
|
||||
// Store the current mode of the program
|
||||
d->options_ = options;
|
||||
|
||||
// Generating an application ID used for identifying the shared memory block and QLocalServer
|
||||
d->genBlockServerName();
|
||||
|
||||
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
|
||||
SingleCoreApplicationPrivate::randomSleep();
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
|
||||
d->memory_ = new QSharedMemory(d->blockServerName_);
|
||||
d->memory_->attach();
|
||||
delete d->memory_;
|
||||
#endif
|
||||
|
||||
// Guarantee thread safe behaviour with a shared memory block.
|
||||
d->memory_ = new QSharedMemory(d->blockServerName_);
|
||||
|
||||
// Create a shared memory block
|
||||
if (d->memory_->create(sizeof(InstancesInfo))) {
|
||||
// Initialize the shared memory block
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleCoreApplication: Unable to lock memory block after create.";
|
||||
abortSafely();
|
||||
}
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
else {
|
||||
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
|
||||
// Attempt to attach to the memory segment
|
||||
if (!d->memory_->attach()) {
|
||||
qCritical() << "SingleCoreApplication: Unable to attach to shared memory block.";
|
||||
abortSafely();
|
||||
}
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleCoreApplication: Unable to lock memory block after attach.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qCritical() << "SingleCoreApplication: Unable to create block.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Make sure the shared memory block is initialised and in consistent state
|
||||
forever {
|
||||
// If the shared memory block's checksum is valid continue
|
||||
if (d->blockChecksum() == instance->checksum) break;
|
||||
|
||||
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
|
||||
if (time.elapsed() > 5000) {
|
||||
qWarning() << "SingleCoreApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
|
||||
// Otherwise wait for a random period and try again.
|
||||
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleCoreApplication: Unable to unlock memory for random wait.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
SingleCoreApplicationPrivate::randomSleep();
|
||||
if (!d->memory_->lock()) {
|
||||
qCritical() << "SingleCoreApplication: Unable to lock memory after random wait.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
if (!instance->primary) {
|
||||
d->startPrimary();
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleCoreApplication: Unable to unlock memory after primary start.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if another instance can be started
|
||||
if (allowSecondary) {
|
||||
d->startSecondary();
|
||||
if (d->options_ & Mode::SecondaryNotification) {
|
||||
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::SecondaryInstance);
|
||||
}
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleCoreApplication: Unable to unlock memory after secondary start.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d->memory_->unlock()) {
|
||||
qDebug() << "SingleCoreApplication: Unable to unlock memory at end of execution.";
|
||||
qDebug() << d->memory_->errorString();
|
||||
}
|
||||
|
||||
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::NewInstance);
|
||||
|
||||
delete d;
|
||||
|
||||
::exit(EXIT_SUCCESS);
|
||||
|
||||
}
|
||||
|
||||
SingleCoreApplication::~SingleCoreApplication() {
|
||||
Q_D(SingleCoreApplication);
|
||||
delete d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is primary.
|
||||
* @return Returns true if the instance is primary, false otherwise.
|
||||
*/
|
||||
bool SingleCoreApplication::isPrimary() const {
|
||||
Q_D(const SingleCoreApplication);
|
||||
return d->server_ != nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is secondary.
|
||||
* @return Returns true if the instance is secondary, false otherwise.
|
||||
*/
|
||||
bool SingleCoreApplication::isSecondary() const {
|
||||
Q_D(const SingleCoreApplication);
|
||||
return d->server_ == nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to identify an instance by returning unique consecutive instance ids.
|
||||
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
|
||||
* @return Returns a unique instance id.
|
||||
*/
|
||||
quint32 SingleCoreApplication::instanceId() const {
|
||||
Q_D(const SingleCoreApplication);
|
||||
return d->instanceNumber_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OS PID (Process Identifier) of the process running the primary instance.
|
||||
* Especially useful when SingleCoreApplication is coupled with OS. specific APIs.
|
||||
* @return Returns the primary instance PID.
|
||||
*/
|
||||
qint64 SingleCoreApplication::primaryPid() const {
|
||||
Q_D(const SingleCoreApplication);
|
||||
return d->primaryPid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the primary instance is running as.
|
||||
* @return Returns the username the primary instance is running as.
|
||||
*/
|
||||
QString SingleCoreApplication::primaryUser() const {
|
||||
Q_D(const SingleCoreApplication);
|
||||
return d->primaryUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the current instance is running as.
|
||||
* @return Returns the username the current instance is running as.
|
||||
*/
|
||||
QString SingleCoreApplication::currentUser() const {
|
||||
return SingleCoreApplicationPrivate::getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the Primary Instance.
|
||||
* @param message The message to send.
|
||||
* @param timeout the maximum timeout in milliseconds for blocking functions.
|
||||
* @return true if the message was sent successfully, false otherwise.
|
||||
*/
|
||||
bool SingleCoreApplication::sendMessage(const QByteArray &message, const int timeout) {
|
||||
|
||||
Q_D(SingleCoreApplication);
|
||||
|
||||
// Nobody to connect to
|
||||
if (isPrimary()) return false;
|
||||
|
||||
// Make sure the socket is connected
|
||||
if (!d->connectToPrimary(timeout, SingleCoreApplicationPrivate::Reconnect)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return d->writeConfirmedMessage(timeout, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the shared memory block and exits with a failure.
|
||||
* This function halts program execution.
|
||||
*/
|
||||
void SingleCoreApplication::abortSafely() {
|
||||
|
||||
Q_D(SingleCoreApplication);
|
||||
|
||||
qCritical() << "SingleCoreApplication: " << d->memory_->error() << d->memory_->errorString();
|
||||
delete d;
|
||||
|
||||
::exit(EXIT_FAILURE);
|
||||
|
||||
}
|
||||
152
3rdparty/singleapplication/singlecoreapplication.h
vendored
152
3rdparty/singleapplication/singlecoreapplication.h
vendored
@@ -1,152 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#ifndef SINGLECOREAPPLICATION_H
|
||||
#define SINGLECOREAPPLICATION_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QCoreApplication>
|
||||
#include <QFlags>
|
||||
#include <QByteArray>
|
||||
|
||||
class SingleCoreApplicationPrivate;
|
||||
|
||||
/**
|
||||
* @brief The SingleCoreApplication class handles multiple instances of the same Application
|
||||
* @see QCoreApplication
|
||||
*/
|
||||
class SingleCoreApplication : public QCoreApplication { // clazy:exclude=ctor-missing-parent-argument
|
||||
Q_OBJECT
|
||||
|
||||
using app_t = QCoreApplication;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Mode of operation of SingleCoreApplication.
|
||||
* Whether the block should be user-wide or system-wide and whether the
|
||||
* primary instance should be notified when a secondary instance had been
|
||||
* started.
|
||||
* @note Operating system can restrict the shared memory blocks to the same
|
||||
* user, in which case the User/System modes will have no effect and the
|
||||
* block will be user wide.
|
||||
* @enum
|
||||
*/
|
||||
enum Mode {
|
||||
User = 1 << 0,
|
||||
System = 1 << 1,
|
||||
SecondaryNotification = 1 << 2,
|
||||
ExcludeAppVersion = 1 << 3,
|
||||
ExcludeAppPath = 1 << 4
|
||||
};
|
||||
Q_DECLARE_FLAGS(Options, Mode)
|
||||
|
||||
/**
|
||||
* @brief Intitializes a SingleCoreApplication instance with argc command line
|
||||
* arguments in argv
|
||||
* @arg {int &} argc - Number of arguments in argv
|
||||
* @arg {const char *[]} argv - Supplied command line arguments
|
||||
* @arg {bool} allowSecondary - Whether to start the instance as secondary
|
||||
* if there is already a primary instance.
|
||||
* @arg {Mode} mode - Whether for the SingleCoreApplication block to be applied
|
||||
* User wide or System wide.
|
||||
* @arg {int} timeout - Timeout to wait in milliseconds.
|
||||
* @note argc and argv may be changed as Qt removes arguments that it
|
||||
* recognizes
|
||||
* @note Mode::SecondaryNotification only works if set on both the primary
|
||||
* instance and the secondary instance.
|
||||
* @note The timeout is just a hint for the maximum time of blocking
|
||||
* operations. It does not guarantee that the SingleCoreApplication
|
||||
* initialisation will be completed in given time, though is a good hint.
|
||||
* Usually 4*timeout would be the worst case (fail) scenario.
|
||||
*/
|
||||
explicit SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
|
||||
~SingleCoreApplication() override;
|
||||
|
||||
/**
|
||||
* @brief Returns if the instance is the primary instance
|
||||
* @returns {bool}
|
||||
*/
|
||||
bool isPrimary() const;
|
||||
|
||||
/**
|
||||
* @brief Returns if the instance is a secondary instance
|
||||
* @returns {bool}
|
||||
*/
|
||||
bool isSecondary() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a unique identifier for the current instance
|
||||
* @returns {qint32}
|
||||
*/
|
||||
quint32 instanceId() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the process ID (PID) of the primary instance
|
||||
* @returns {qint64}
|
||||
*/
|
||||
qint64 primaryPid() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the username of the user running the primary instance
|
||||
* @returns {QString}
|
||||
*/
|
||||
QString primaryUser() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the username of the current user
|
||||
* @returns {QString}
|
||||
*/
|
||||
QString currentUser() const;
|
||||
|
||||
/**
|
||||
* @brief Sends a message to the primary instance. Returns true on success.
|
||||
* @param {int} timeout - Timeout for connecting
|
||||
* @returns {bool}
|
||||
* @note sendMessage() will return false if invoked from the primary
|
||||
* instance.
|
||||
*/
|
||||
bool sendMessage(const QByteArray &message, const int timeout = 1000);
|
||||
|
||||
signals:
|
||||
void instanceStarted();
|
||||
void receivedMessage(quint32 instanceId, QByteArray message);
|
||||
|
||||
private:
|
||||
SingleCoreApplicationPrivate *d_ptr;
|
||||
Q_DECLARE_PRIVATE(SingleCoreApplication)
|
||||
void abortSafely();
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleCoreApplication::Options)
|
||||
|
||||
#endif // SINGLECOREAPPLICATION_H
|
||||
17
3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt
vendored
Normal file
17
3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
add_definitions(-DSINGLECOREAPPLICATION)
|
||||
|
||||
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
|
||||
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
add_library(singlecoreapplication STATIC ${SOURCES} ${MOC})
|
||||
target_include_directories(singlecoreapplication PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
||||
${Boost_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(singlecoreapplication PUBLIC
|
||||
${QtCore_LIBRARIES}
|
||||
${QtNetwork_LIBRARIES}
|
||||
)
|
||||
13
3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h
vendored
Normal file
13
3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef SINGLECOREAPPLICATION_H
|
||||
#define SINGLECOREAPPLICATION_H
|
||||
|
||||
#ifdef SINGLECOREAPPLICATION
|
||||
# error "SINGLECOREAPPLICATION already defined."
|
||||
#endif
|
||||
|
||||
#define SINGLECOREAPPLICATION
|
||||
#include "../singleapplication_t.h"
|
||||
#undef SINGLEAPPLICATION_T_H
|
||||
#undef SINGLECOREAPPLICATION
|
||||
|
||||
#endif // SINGLECOREAPPLICATION_H
|
||||
@@ -1,526 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
# include <unistd.h>
|
||||
# include <sys/types.h>
|
||||
# include <pwd.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
# ifndef NOMINMAX
|
||||
# define NOMINMAX 1
|
||||
# endif
|
||||
# include <windows.h>
|
||||
# include <lmcons.h>
|
||||
#endif
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QIODevice>
|
||||
#include <QSharedMemory>
|
||||
#include <QByteArray>
|
||||
#include <QDataStream>
|
||||
#include <QCryptographicHash>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QElapsedTimer>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
# include <QRandomGenerator>
|
||||
#else
|
||||
# include <QDateTime>
|
||||
#endif
|
||||
|
||||
#include "singlecoreapplication.h"
|
||||
#include "singlecoreapplication_p.h"
|
||||
|
||||
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *ptr)
|
||||
: q_ptr(ptr),
|
||||
memory_(nullptr),
|
||||
socket_(nullptr),
|
||||
server_(nullptr),
|
||||
instanceNumber_(-1) {}
|
||||
|
||||
SingleCoreApplicationPrivate::~SingleCoreApplicationPrivate() {
|
||||
|
||||
if (socket_ != nullptr) {
|
||||
socket_->close();
|
||||
delete socket_;
|
||||
socket_ = nullptr;
|
||||
}
|
||||
|
||||
if (memory_ != nullptr) {
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
if (server_ != nullptr) {
|
||||
server_->close();
|
||||
delete server_;
|
||||
instance->primary = false;
|
||||
instance->primaryPid = -1;
|
||||
instance->primaryUser[0] = '\0';
|
||||
instance->checksum = blockChecksum();
|
||||
}
|
||||
memory_->unlock();
|
||||
|
||||
delete memory_;
|
||||
memory_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString SingleCoreApplicationPrivate::getUsername() {
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
QString username;
|
||||
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
|
||||
struct passwd *pw = getpwuid(geteuid());
|
||||
if (pw) {
|
||||
username = QString::fromLocal8Bit(pw->pw_name);
|
||||
}
|
||||
#endif
|
||||
if (username.isEmpty()) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
username = qEnvironmentVariable("USER");
|
||||
#else
|
||||
username = QString::fromLocal8Bit(qgetenv("USER"));
|
||||
#endif
|
||||
}
|
||||
return username;
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
wchar_t username[UNLEN + 1];
|
||||
// Specifies size of the buffer on input
|
||||
DWORD usernameLength = UNLEN + 1;
|
||||
if (GetUserNameW(username, &usernameLength)) {
|
||||
return QString::fromWCharArray(username);
|
||||
}
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
return qEnvironmentVariable("USERNAME");
|
||||
#else
|
||||
return QString::fromLocal8Bit(qgetenv("USERNAME"));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::genBlockServerName() {
|
||||
|
||||
QCryptographicHash appData(QCryptographicHash::Sha256);
|
||||
appData.addData("SingleApplication");
|
||||
appData.addData(SingleCoreApplication::app_t::applicationName().toUtf8());
|
||||
appData.addData(SingleCoreApplication::app_t::organizationName().toUtf8());
|
||||
appData.addData(SingleCoreApplication::app_t::organizationDomain().toUtf8());
|
||||
|
||||
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppVersion)) {
|
||||
appData.addData(SingleCoreApplication::app_t::applicationVersion().toUtf8());
|
||||
}
|
||||
|
||||
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppPath)) {
|
||||
#if defined(Q_OS_UNIX)
|
||||
const QByteArray appImagePath = qgetenv("APPIMAGE");
|
||||
if (appImagePath.isEmpty()) {
|
||||
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
|
||||
}
|
||||
else {
|
||||
appData.addData(appImagePath);
|
||||
};
|
||||
#elif defined(Q_OS_WIN)
|
||||
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toLower().toUtf8());
|
||||
#else
|
||||
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
|
||||
#endif
|
||||
}
|
||||
|
||||
// User level block requires a user specific data in the hash
|
||||
if (options_ & SingleCoreApplication::Mode::User) {
|
||||
appData.addData(getUsername().toUtf8());
|
||||
}
|
||||
|
||||
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with server naming requirements.
|
||||
blockServerName_ = appData.result().toBase64().replace("/", "_");
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::initializeMemoryBlock() const {
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
instance->primary = false;
|
||||
instance->secondary = 0;
|
||||
instance->primaryPid = -1;
|
||||
instance->primaryUser[0] = '\0';
|
||||
instance->checksum = blockChecksum();
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::startPrimary() {
|
||||
|
||||
// Reset the number of connections
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
|
||||
instance->primary = true;
|
||||
instance->primaryPid = QCoreApplication::applicationPid();
|
||||
qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser));
|
||||
instance->checksum = blockChecksum();
|
||||
instanceNumber_ = 0;
|
||||
// Successful creation means that no main process exists
|
||||
// So we start a QLocalServer to listen for connections
|
||||
QLocalServer::removeServer(blockServerName_);
|
||||
server_ = new QLocalServer();
|
||||
|
||||
// Restrict access to the socket according to the SingleCoreApplication::Mode::User flag on User level or no restrictions
|
||||
if (options_ & SingleCoreApplication::Mode::User) {
|
||||
server_->setSocketOptions(QLocalServer::UserAccessOption);
|
||||
}
|
||||
else {
|
||||
server_->setSocketOptions(QLocalServer::WorldAccessOption);
|
||||
}
|
||||
|
||||
server_->listen(blockServerName_);
|
||||
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleCoreApplicationPrivate::slotConnectionEstablished);
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::startSecondary() {
|
||||
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
|
||||
instance->secondary += 1;
|
||||
instance->checksum = blockChecksum();
|
||||
instanceNumber_ = instance->secondary;
|
||||
|
||||
}
|
||||
|
||||
bool SingleCoreApplicationPrivate::connectToPrimary(const int timeout, const ConnectionType connectionType) {
|
||||
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Connect to the Local Server of the Primary Instance if not already connected.
|
||||
if (socket_ == nullptr) {
|
||||
socket_ = new QLocalSocket();
|
||||
}
|
||||
|
||||
if (socket_->state() == QLocalSocket::ConnectedState) return true;
|
||||
|
||||
if (socket_->state() != QLocalSocket::ConnectedState) {
|
||||
|
||||
forever {
|
||||
randomSleep();
|
||||
|
||||
if (socket_->state() != QLocalSocket::ConnectingState) {
|
||||
socket_->connectToServer(blockServerName_);
|
||||
}
|
||||
|
||||
if (socket_->state() == QLocalSocket::ConnectingState) {
|
||||
socket_->waitForConnected(static_cast<int>(timeout - time.elapsed()));
|
||||
}
|
||||
|
||||
// If connected break out of the loop
|
||||
if (socket_->state() == QLocalSocket::ConnectedState) break;
|
||||
|
||||
// If elapsed time since start is longer than the method timeout return
|
||||
if (time.elapsed() >= timeout) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation message according to the SingleCoreApplication protocol
|
||||
QByteArray initMsg;
|
||||
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
||||
writeStream.setVersion(QDataStream::Qt_5_8);
|
||||
|
||||
writeStream << blockServerName_.toLatin1();
|
||||
writeStream << static_cast<quint8>(connectionType);
|
||||
writeStream << instanceNumber_;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
|
||||
#else
|
||||
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
||||
#endif
|
||||
|
||||
writeStream << checksum;
|
||||
|
||||
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::writeAck(QLocalSocket *sock) {
|
||||
sock->putChar('\n');
|
||||
}
|
||||
|
||||
bool SingleCoreApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
|
||||
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Frame 1: The header indicates the message length that follows
|
||||
QByteArray header;
|
||||
QDataStream headerStream(&header, QIODevice::WriteOnly);
|
||||
headerStream.setVersion(QDataStream::Qt_5_8);
|
||||
headerStream << static_cast<quint64>(msg.length());
|
||||
|
||||
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Frame 2: The message
|
||||
return writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), msg);
|
||||
|
||||
}
|
||||
|
||||
bool SingleCoreApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
|
||||
|
||||
socket_->write(msg);
|
||||
socket_->flush();
|
||||
|
||||
bool result = socket_->waitForReadyRead(timeout);
|
||||
if (result) {
|
||||
socket_->read(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
quint16 SingleCoreApplicationPrivate::blockChecksum() const {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
|
||||
#else
|
||||
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
|
||||
#endif
|
||||
|
||||
return checksum;
|
||||
|
||||
}
|
||||
|
||||
qint64 SingleCoreApplicationPrivate::primaryPid() const {
|
||||
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
qint64 pid = instance->primaryPid;
|
||||
memory_->unlock();
|
||||
|
||||
return pid;
|
||||
|
||||
}
|
||||
|
||||
QString SingleCoreApplicationPrivate::primaryUser() const {
|
||||
|
||||
memory_->lock();
|
||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||
QByteArray username = instance->primaryUser;
|
||||
memory_->unlock();
|
||||
|
||||
return QString::fromUtf8(username);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Executed when a connection has been made to the LocalServer
|
||||
*/
|
||||
void SingleCoreApplicationPrivate::slotConnectionEstablished() {
|
||||
|
||||
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
|
||||
connectionMap_.insert(nextConnSocket, ConnectionInfo());
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
|
||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
||||
slotClientConnectionClosed(nextConnSocket, info.instanceId);
|
||||
});
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
|
||||
connectionMap_.remove(nextConnSocket);
|
||||
});
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
|
||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
||||
switch (info.stage) {
|
||||
case StageInitHeader:
|
||||
readMessageHeader(nextConnSocket, StageInitBody);
|
||||
break;
|
||||
case StageInitBody:
|
||||
readInitMessageBody(nextConnSocket);
|
||||
break;
|
||||
case StageConnectedHeader:
|
||||
readMessageHeader(nextConnSocket, StageConnectedBody);
|
||||
break;
|
||||
case StageConnectedBody:
|
||||
this->slotDataAvailable(nextConnSocket, info.instanceId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::readMessageHeader(QLocalSocket *sock, SingleCoreApplicationPrivate::ConnectionStage nextStage) {
|
||||
|
||||
if (!connectionMap_.contains(sock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sock->bytesAvailable() < static_cast<qint64>(sizeof(quint64))) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDataStream headerStream(sock);
|
||||
headerStream.setVersion(QDataStream::Qt_5_8);
|
||||
|
||||
// Read the header to know the message length
|
||||
quint64 msgLen = 0;
|
||||
headerStream >> msgLen;
|
||||
ConnectionInfo &info = connectionMap_[sock];
|
||||
info.stage = nextStage;
|
||||
info.msgLen = msgLen;
|
||||
|
||||
writeAck(sock);
|
||||
|
||||
}
|
||||
|
||||
bool SingleCoreApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
|
||||
|
||||
if (!connectionMap_.contains(sock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ConnectionInfo &info = connectionMap_[sock];
|
||||
return (sock->bytesAvailable() >= static_cast<qint64>(info.msgLen));
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
|
||||
|
||||
Q_Q(SingleCoreApplication);
|
||||
|
||||
if (!isFrameComplete(sock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the message body
|
||||
QByteArray msgBytes = sock->readAll();
|
||||
QDataStream readStream(msgBytes);
|
||||
readStream.setVersion(QDataStream::Qt_5_8);
|
||||
|
||||
// server name
|
||||
QByteArray latin1Name;
|
||||
readStream >> latin1Name;
|
||||
|
||||
// connection type
|
||||
ConnectionType connectionType = InvalidConnection;
|
||||
quint8 connTypeVal = InvalidConnection;
|
||||
readStream >> connTypeVal;
|
||||
connectionType = static_cast<ConnectionType>(connTypeVal);
|
||||
|
||||
// instance id
|
||||
quint32 instanceId = 0;
|
||||
readStream >> instanceId;
|
||||
|
||||
// checksum
|
||||
quint16 msgChecksum = 0;
|
||||
readStream >> msgChecksum;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
|
||||
#else
|
||||
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
|
||||
#endif
|
||||
|
||||
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum;
|
||||
|
||||
if (!isValid) {
|
||||
sock->close();
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectionInfo &info = connectionMap_[sock];
|
||||
info.instanceId = instanceId;
|
||||
info.stage = StageConnectedHeader;
|
||||
|
||||
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleCoreApplication::Mode::SecondaryNotification)) {
|
||||
emit q->instanceStarted();
|
||||
}
|
||||
|
||||
writeAck(sock);
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
|
||||
|
||||
Q_Q(SingleCoreApplication);
|
||||
|
||||
if (!isFrameComplete(dataSocket)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray message = dataSocket->readAll();
|
||||
|
||||
writeAck(dataSocket);
|
||||
|
||||
ConnectionInfo &info = connectionMap_[dataSocket];
|
||||
info.stage = StageConnectedHeader;
|
||||
|
||||
emit q->receivedMessage(instanceId, message);
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
|
||||
|
||||
if (closedSocket->bytesAvailable() > 0) {
|
||||
slotDataAvailable(closedSocket, instanceId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SingleCoreApplicationPrivate::randomSleep() {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
|
||||
#else
|
||||
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
|
||||
QThread::msleep(qrand() % 11 + 8);
|
||||
#endif
|
||||
|
||||
}
|
||||
116
3rdparty/singleapplication/singlecoreapplication_p.h
vendored
116
3rdparty/singleapplication/singlecoreapplication_p.h
vendored
@@ -1,116 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This is a modified version of SingleApplication,
|
||||
// The original version is at:
|
||||
//
|
||||
// https://github.com/itay-grudev/SingleApplication
|
||||
//
|
||||
//
|
||||
|
||||
#ifndef SINGLECOREAPPLICATION_P_H
|
||||
#define SINGLECOREAPPLICATION_P_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
|
||||
#include "singlecoreapplication.h"
|
||||
|
||||
class QLocalServer;
|
||||
class QLocalSocket;
|
||||
class QSharedMemory;
|
||||
|
||||
struct InstancesInfo {
|
||||
bool primary;
|
||||
quint32 secondary;
|
||||
qint64 primaryPid;
|
||||
char primaryUser[128];
|
||||
quint16 checksum;
|
||||
};
|
||||
|
||||
struct ConnectionInfo {
|
||||
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
|
||||
quint64 msgLen;
|
||||
quint32 instanceId;
|
||||
quint8 stage;
|
||||
};
|
||||
|
||||
class SingleCoreApplicationPrivate : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ConnectionType : quint8 {
|
||||
InvalidConnection = 0,
|
||||
NewInstance = 1,
|
||||
SecondaryInstance = 2,
|
||||
Reconnect = 3
|
||||
};
|
||||
enum ConnectionStage : quint8 {
|
||||
StageInitHeader = 0,
|
||||
StageInitBody = 1,
|
||||
StageConnectedHeader = 2,
|
||||
StageConnectedBody = 3,
|
||||
};
|
||||
Q_DECLARE_PUBLIC(SingleCoreApplication)
|
||||
|
||||
explicit SingleCoreApplicationPrivate(SingleCoreApplication *ptr);
|
||||
~SingleCoreApplicationPrivate() override;
|
||||
|
||||
static QString getUsername();
|
||||
void genBlockServerName();
|
||||
void initializeMemoryBlock() const;
|
||||
void startPrimary();
|
||||
void startSecondary();
|
||||
bool connectToPrimary(const int timeout, const ConnectionType connectionType);
|
||||
quint16 blockChecksum() const;
|
||||
qint64 primaryPid() const;
|
||||
QString primaryUser() const;
|
||||
bool isFrameComplete(QLocalSocket *sock);
|
||||
void readMessageHeader(QLocalSocket *socket, const ConnectionStage nextStage);
|
||||
void readInitMessageBody(QLocalSocket *socket);
|
||||
void writeAck(QLocalSocket *sock);
|
||||
bool writeConfirmedFrame(const int timeout, const QByteArray &msg) const;
|
||||
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
|
||||
static void randomSleep();
|
||||
|
||||
SingleCoreApplication *q_ptr;
|
||||
QSharedMemory *memory_;
|
||||
QLocalSocket *socket_;
|
||||
QLocalServer *server_;
|
||||
quint32 instanceNumber_;
|
||||
QString blockServerName_;
|
||||
SingleCoreApplication::Options options_;
|
||||
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
|
||||
|
||||
public slots:
|
||||
void slotConnectionEstablished();
|
||||
void slotDataAvailable(QLocalSocket*, const quint32);
|
||||
void slotClientConnectionClosed(QLocalSocket*, const quint32);
|
||||
};
|
||||
|
||||
#endif // SINGLECOREAPPLICATION_P_H
|
||||
@@ -1,7 +1,13 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
project(strawberry)
|
||||
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_policy(SET CMP0054 NEW)
|
||||
if(POLICY CMP0054)
|
||||
cmake_policy(SET CMP0054 NEW)
|
||||
endif()
|
||||
if(POLICY CMP0074)
|
||||
cmake_policy(SET CMP0074 NEW)
|
||||
endif()
|
||||
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(CheckCXXSourceRuns)
|
||||
@@ -12,13 +18,13 @@ include(cmake/Summary.cmake)
|
||||
include(cmake/OptionalSource.cmake)
|
||||
include(cmake/ParseArguments.cmake)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(LINUX ON)
|
||||
endif()
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
|
||||
set(FREEBSD ON)
|
||||
endif()
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
|
||||
set(OPENBSD ON)
|
||||
endif()
|
||||
|
||||
@@ -68,23 +74,22 @@ else()
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>
|
||||
)
|
||||
endif()
|
||||
|
||||
option(BUILD_WERROR "Build with -Werror" OFF)
|
||||
if(BUILD_WERROR)
|
||||
list(APPEND COMPILE_OPTIONS -Werror)
|
||||
option(BUILD_WERROR "Build with -Werror" OFF)
|
||||
if(BUILD_WERROR)
|
||||
list(APPEND COMPILE_OPTIONS -Werror)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_compile_options(${COMPILE_OPTIONS})
|
||||
|
||||
if(${CMAKE_BUILD_TYPE} MATCHES "Release")
|
||||
if(CMAKE_BUILD_TYPE MATCHES "Release")
|
||||
add_definitions(-DNDEBUG)
|
||||
add_definitions(-DQT_NO_DEBUG_OUTPUT)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks")
|
||||
option(USE_RPATH "Use RPATH" APPLE)
|
||||
if(USE_RPATH)
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||
endif()
|
||||
|
||||
find_program(CCACHE_EXECUTABLE NAMES ccache)
|
||||
@@ -94,6 +99,8 @@ if(CCACHE_EXECUTABLE)
|
||||
SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_EXECUTABLE})
|
||||
endif()
|
||||
|
||||
option(USE_ICU "Use ICU" ON)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(Threads)
|
||||
@@ -101,7 +108,14 @@ find_package(Backtrace)
|
||||
if(Backtrace_FOUND)
|
||||
set(HAVE_BACKTRACE ON)
|
||||
endif()
|
||||
find_package(Iconv)
|
||||
if(USE_ICU)
|
||||
find_package(ICU COMPONENTS uc i18n REQUIRED)
|
||||
if(ICU_FOUND)
|
||||
set(HAVE_ICU ON)
|
||||
endif()
|
||||
else()
|
||||
find_package(Iconv)
|
||||
endif()
|
||||
find_package(GnuTLS REQUIRED)
|
||||
find_package(Protobuf REQUIRED)
|
||||
if(NOT Protobuf_PROTOC_EXECUTABLE)
|
||||
@@ -297,7 +311,10 @@ endif()
|
||||
|
||||
# SingleApplication
|
||||
add_subdirectory(3rdparty/singleapplication)
|
||||
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication)
|
||||
set(SINGLEAPPLICATION_INCLUDE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singleapplication
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singlecoreapplication
|
||||
)
|
||||
set(SINGLEAPPLICATION_LIBRARIES singleapplication)
|
||||
set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication)
|
||||
|
||||
@@ -364,12 +381,12 @@ optional_component(VLC ON "Engine: VLC backend"
|
||||
|
||||
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
|
||||
DEPENDS "chromaprint" CHROMAPRINT_FOUND
|
||||
DEPENDS "gstreamer" GSTREAMER_FOUND
|
||||
DEPENDS "gstreamer" HAVE_GSTREAMER
|
||||
)
|
||||
|
||||
optional_component(MUSICBRAINZ ON "MusicBrainz integration"
|
||||
DEPENDS "chromaprint" CHROMAPRINT_FOUND
|
||||
DEPENDS "gstreamer" GSTREAMER_FOUND
|
||||
DEPENDS "gstreamer" HAVE_GSTREAMER
|
||||
)
|
||||
|
||||
if(X11_FOUND OR HAVE_DBUS OR APPLE OR WIN32)
|
||||
@@ -394,7 +411,7 @@ endif()
|
||||
|
||||
optional_component(AUDIOCD ON "Devices: Audio CD support"
|
||||
DEPENDS "libcdio" LIBCDIO_FOUND
|
||||
DEPENDS "gstreamer" GSTREAMER_FOUND
|
||||
DEPENDS "gstreamer" HAVE_GSTREAMER
|
||||
)
|
||||
|
||||
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
|
||||
@@ -500,9 +517,12 @@ endif()
|
||||
add_definitions(
|
||||
-DBOOST_BIND_NO_PLACEHOLDERS
|
||||
-DQT_STRICT_ITERATORS
|
||||
-DQT_NO_CAST_FROM_BYTEARRAY
|
||||
-DQT_USE_QSTRINGBUILDER
|
||||
-DQT_NO_URL_CAST_FROM_STRING
|
||||
-DQT_NO_CAST_TO_ASCII
|
||||
-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT
|
||||
-DQT_NO_FOREACH
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
112
CONTRIBUTING.md
Normal file
112
CONTRIBUTING.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Contribution guidelines
|
||||
|
||||
Strawberry is an free and open-source project, it is possible and encouraged to participate in the development.
|
||||
You can also participate by answering questions, reporting bugs or helping with documentation.
|
||||
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
You should start by creating a fork of the Strawberry repository using the GitHub
|
||||
fork button, after that you can clone the repository from your fork.
|
||||
Replace "username" with your own.
|
||||
|
||||
|
||||
### Clone the repository
|
||||
|
||||
git clone git@github.com:username/strawberry.git
|
||||
cd strawberry
|
||||
|
||||
|
||||
### Setup the remote
|
||||
|
||||
git remote add upstream git@github.com:strawberrymusicplayer/strawberry.git
|
||||
|
||||
|
||||
### Create a new branch
|
||||
|
||||
This creates a new branch from the master branch that you use for specific
|
||||
changes.
|
||||
|
||||
git checkout -b your-branch
|
||||
|
||||
|
||||
### Stage changes
|
||||
|
||||
Once you've finished working on a specific change, stage the changes for
|
||||
a specific commit.
|
||||
|
||||
Always keep your commits relevant to the pull request, and each commit as
|
||||
small as possible.
|
||||
|
||||
git add -p
|
||||
|
||||
|
||||
### Commit changes
|
||||
|
||||
git commit
|
||||
|
||||
|
||||
### Commit messages
|
||||
|
||||
The first line should start with "Class:", which referer to the class
|
||||
that is changed. Don't use a trailing period after the first line.
|
||||
If this change affects more than one class, omit the class and write a
|
||||
more general message.
|
||||
You only need to include a main description (body) for larger changes
|
||||
where the one line is not enough to describe everything.
|
||||
The main description starts after two newlines, it is normal prose and
|
||||
should use normal punctuation and capital letters where appropriate.
|
||||
|
||||
An example of the expected format for git commit messages is as follows:
|
||||
|
||||
```
|
||||
class: Short explanation of the commit
|
||||
|
||||
Longer explanation explaining exactly what's changed, why it's changed,
|
||||
and what bugs were fixed.
|
||||
|
||||
Fixes #1234
|
||||
```
|
||||
|
||||
|
||||
### Push the changes to GitHub
|
||||
|
||||
Once you've finished working on the changes, push the branch
|
||||
to the Git repository and open a new pull request.
|
||||
|
||||
|
||||
git push origin your-branch
|
||||
|
||||
|
||||
### Update your fork's master branch
|
||||
|
||||
git checkout master
|
||||
git pull --rebase origin master
|
||||
git fetch upstream
|
||||
git merge upstream/master
|
||||
git push origin master
|
||||
|
||||
|
||||
### Update your branch
|
||||
|
||||
git checkout your-branch
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
git push origin your-branch --force-with-lease
|
||||
|
||||
|
||||
### Rebase your branch
|
||||
|
||||
If you need fix any issues with your commits, you need to rebase your
|
||||
branch to squash any commits, or to change the commit message.
|
||||
|
||||
git checkout your-branch
|
||||
git log
|
||||
git rebase -i commit_sha~
|
||||
git push origin your-branch --force-with-lease
|
||||
|
||||
|
||||
### Delete your fork
|
||||
|
||||
If you do not plan to work more on Strawberry, please delete your fork from GitHub
|
||||
once the pull requests are merged.
|
||||
146
Changelog
146
Changelog
@@ -2,6 +2,144 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.0.17 (2023.03.29):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed over-sized context album cover with device pixel ratio higher than 1.0 (#1166).
|
||||
* Fixed playing widget fading from a blurry previous cover with device pixel ratio higher than 1.0.
|
||||
* Made playlist source icon, album cover manager and OSD pretty cover respect device pixel ratio.
|
||||
|
||||
Version 1.0.16 (2023.03.27):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed lyrics from Musixmatch.
|
||||
* Fixed possible file corruption when saving both tags and embedded cover using the tag editor (#1158).
|
||||
* Fixed compile without GStreamer.
|
||||
* Fixed context and playing now album art rendering on High DPI displays (#1161).
|
||||
* Fixed setting source properties (device, user-agent, ssl-strict) with GStreamer 1.22 (playbin3) and higher (#1148).
|
||||
* Fixed rescan songs feature not ignoring mtime.
|
||||
* Search lyrics by artist instead of album artist by default.
|
||||
|
||||
Code improvements:
|
||||
* Replace use of deprecated QSqlDatabase::exec().
|
||||
|
||||
Added features:
|
||||
* Added backend setting for strict SSL mode.
|
||||
* Read AcoustID and MusicBrainz tags.
|
||||
* Submit MusicBrainz tags with ListenBrainz.
|
||||
|
||||
Version 1.0.15 (2023.03.04):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed playlist column showing invalid last played date for streams.
|
||||
* Fixed crash when the audio bin failed to initialize (#1123, #1133).
|
||||
* Fixed duplicated filename when organizing files using dot in the filename (#1136).
|
||||
* Fixed tag inline editing for streams (#1130).
|
||||
* Fixed resetting play statistics using tag edit dialog (#1124).
|
||||
* Fixed compilation songs not showing if group by was set to other than (Album) Artist / Album (#1140).
|
||||
|
||||
Enhancements:
|
||||
* Added lyrics from stands4 (lyrics.com).
|
||||
* Added Sonogram analyzer.
|
||||
* Use GStreamer playbin3 with GStreamer 1.22.0 and higher.
|
||||
|
||||
Code improvements:
|
||||
* Made use of C++11 enum class where possible.
|
||||
* Use new QNativeIpcKey based QSharedMemory constructor with Qt 6.6 and higher.
|
||||
|
||||
Version 1.0.14 (2023.01.13):
|
||||
|
||||
Bugfixes:
|
||||
* Fix initial volume not set when using Auto as output (#1104).
|
||||
* Fix saving moodbar if the URL contains host, ie.: UNC paths for SMB (#1101).
|
||||
* Fix CollectionBackendTest compile error (#1100).
|
||||
* Remove explicitly enabling debug messages (#1106).
|
||||
|
||||
Version 1.0.13 (2023.01.09):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed volume synchronization leading to infinite loop resulting in crash when adjusting volume while playing (#1089).
|
||||
* Fixed incorrect volume.
|
||||
* Fixed collection organizing incorrectly handling slashes inside {} brackets for variables (#1091).
|
||||
* Fixed saving relative playlists to non-existing playlist files (#1092).
|
||||
* Fixed intermittent crash on collection model query (#1095).
|
||||
* Require system icons for fancy tabbar and settings sidebar to be larger than 22x22 (#1084).
|
||||
|
||||
Version 1.0.12 (2023.01.02):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed crash when adjusting volume with mouse wheel (#1089).
|
||||
* Fixed playback stopping in certain cases where the next track was unavailable (#958).
|
||||
* (Windows) Apply patch for fonts too large on High DPI screen (QTBUG-108593).
|
||||
|
||||
Removed features:
|
||||
* Removed appearance settings for changing palette colors, it was never properly implemented.
|
||||
|
||||
Version 1.0.11 (2022.12.30):
|
||||
|
||||
Bugfixes:
|
||||
* Capitalize GLib application name so it appears nicely in GNOME and PulseAudio Volume Control (#1066).
|
||||
* Fixed missing application icon for PulseAudio Volume Control (#1066).
|
||||
* Ignore errors for missing albums when updating Tidal collection if there are results (#1061).
|
||||
* Only run periodic collection scan when moitoring collection setting is on.
|
||||
* Fixed an edge case where the context headline text was being cut short (#1067).
|
||||
* Made "Show in file browser" support SpaceFM filemanager (#1073).
|
||||
* Fixed incorrect tab order in edit tag dialog (#1075).
|
||||
* Changed "FMPS_PlayCount" to "FMPS_Playcount" when saving tag (#1074).
|
||||
* Fixed compilation tag read and write for MP4 (#1076).
|
||||
* Removed incorrect use of "TPE1" for performer when reading ID3 tags (#1076).
|
||||
* Disable tag fields for unsupported tags in tag editor.
|
||||
* Don't allow organizing files without unique tags (track or title) for filename (#1077).
|
||||
* Don't remove disc from album title when creating cover hash to allow different covers for each disc on an album (#1069).
|
||||
* Fixed incorrect relative paths for song filenames when saving playlists if the saved playlist location is a symablic link to the song filename (#1071).
|
||||
* Scrobble "Various Artists" as album artist (#1082).
|
||||
|
||||
Enhancements:
|
||||
* Use system volume instead of own software volume when available (#1037).
|
||||
* Improved Tidal and Qobuz support with timed requests.
|
||||
* Support MPRIS2 xesam:userRating.
|
||||
|
||||
Version 1.0.10 (2022.10.21):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed "Could not open settings file for writing: No such file or directory" error before settings file is created.
|
||||
* Fixed visual glitch on currently playing track (#1051).
|
||||
* Fixed "Unknown error" on Tidal search (#1047).
|
||||
* Fixed incomplete lyrics from Genius.
|
||||
* Fixed icons not showing in the file view on some systems (#1024).
|
||||
* Fixed issues with context and playing widget stopping when using VLC (#1054).
|
||||
* (macOS) Fixed search field related crash when playlist toolbar is turned off.
|
||||
|
||||
Enhancements:
|
||||
* Fixed narrowing conversions in connects.
|
||||
* Fixed casts from QByteArray.
|
||||
* Removed subdir for generated dbus files
|
||||
* Removed use of fixed font in context (#1040).
|
||||
* Improve Musixmatch lyrics search.
|
||||
|
||||
Version 1.0.9 (2022.09.03):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed parsing album title from radio stream metadata (#1023).
|
||||
* (macOS) Fixed Strawberry not starting, incorrect rpath for libgcc_s.1.1.dylib (#1025).
|
||||
* (macOS) Fixed HTTP streaming.
|
||||
|
||||
Version 1.0.8 (2022.08.29):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed backslash being appended to filter text when switching playlist (#1005).
|
||||
* Fixed OSD notifications service registering taking too long to timeout when not available.
|
||||
* Fixed radio stream added twice when double-clicked (#1015).
|
||||
* Fixed translating undo and redo buttons (#1017).
|
||||
|
||||
Enhancements:
|
||||
* Use ICU instead of iconv to transliterate characters for filenames.
|
||||
* Make separating albums by grouping tag optional in collection group by album.
|
||||
* Added support for video game music formats VGM and SPC.
|
||||
* Added setting for explicitly turning on HTTP/2 for streaming. Strawberry will set the
|
||||
libsoup SOUP_FORCE_HTTP1 environment variable when the HTTP/2 is not checked (#1016).
|
||||
* (Windows|MSVC) Install Visual C++ runtime redistributable automatically in installer.
|
||||
|
||||
Version 1.0.7 (2022.07.25)
|
||||
|
||||
Bugfixes:
|
||||
@@ -209,7 +347,7 @@ Version 0.9.3 (2021.04.18)
|
||||
Bugfixes:
|
||||
* Fix "Show in file browser" to work with thunar.
|
||||
* Check that the clicked rating position is to the right or left of the rectangle.
|
||||
* Fix rescan when collection directory is removed and readded.
|
||||
* Fix rescan when collection directory is removed and re-added.
|
||||
* Create GLib main event loop on non-glib systems to fix stream discoverer.
|
||||
* (macOS) Fix intermittent abort on startup.
|
||||
* (macOS) Fix Tidal and Qobuz search field not showing.
|
||||
@@ -546,7 +684,7 @@ Version 0.6.10 (2020.05.01)
|
||||
* Made font and font sizes in context configurable.
|
||||
* Splitting artist and song title to the relevant metadata when artist and song title is sent as title separated by a dash in streams.
|
||||
* Added label to show collection pixmap disk cache used in settings.
|
||||
* Icreased default collection pixmap disk cache to 360.
|
||||
* Increased default collection pixmap disk cache to 360.
|
||||
|
||||
New features:
|
||||
* Added back Tidal streaming support.
|
||||
@@ -614,7 +752,7 @@ Version 0.6.7 (2019.11.27)
|
||||
* Fixed "Pressing Previous in player" behaviour setting
|
||||
* Fixed updating compilations where there are spaces or special characters in filenames
|
||||
* Fixed cases where songs were stuck in "Various Artists" because not all songs in
|
||||
the same compilation was removed from the model before readded with actual artist.
|
||||
the same compilation was removed from the model before re-added with actual artist.
|
||||
* Fixed a bug when importing playlists where metadata was reset
|
||||
* Fixed scrobbler to also scrobble songs without album title
|
||||
* Fixed text for replay gain setting not loading in backend setting
|
||||
@@ -967,7 +1105,7 @@ Version 0.1.5 (2018.05.16)
|
||||
|
||||
|
||||
Version 0.1.4 (2018.05.14)
|
||||
* Fixed compliation with clang compiler
|
||||
* Fixed compilation with clang compiler
|
||||
* This release is mainly to get it working on openbsd and freebsd.
|
||||
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -1,4 +1,4 @@
|
||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
=======================
|
||||
[](https://github.com/sponsors/jonaski)
|
||||
[](https://patreon.com/jonaskvinge)
|
||||
@@ -23,6 +23,7 @@ Resources:
|
||||
|
||||
### :bangbang: Opening an issue:
|
||||
|
||||
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
||||
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
||||
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
||||
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
||||
@@ -50,7 +51,7 @@ Funding developers is a way to contribute to open source projects you appreciate
|
||||
* Edit tags on audio files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Song lyrics from [Lyrics.com](https://www.lyrics.com/), [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* Audio equalizer
|
||||
@@ -68,10 +69,11 @@ It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
||||
|
||||
* [CMake](https://cmake.org/)
|
||||
* [GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/) compiler
|
||||
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 5.9 or higher (or Qt 6) with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [Qt 6 or Qt 5.9 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org)
|
||||
* [Protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
@@ -79,6 +81,7 @@ To build Strawberry from source you need the following installed on your system
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/) or [VLC](https://www.videolan.org)
|
||||
* [GnuTLS](https://www.gnutls.org/)
|
||||
* [TagLib 1.11.1 or higher](https://www.taglib.org/) or [TagParser](https://github.com/Martchus/tagparser)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
@@ -100,15 +103,18 @@ You should also install the gstreamer plugins base and good, and optionally bad,
|
||||
### Compile and install:
|
||||
|
||||
cd strawberry
|
||||
mkdir build && cd build
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DBUILD_WITH_QT6=ON
|
||||
make -j$(nproc)
|
||||
make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
Strawberry is backwards compatible with Qt 5, to compile with Qt 5 use:
|
||||
|
||||
cmake .. -DBUILD_WITH_QT5=ON
|
||||
|
||||
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 0)
|
||||
set(STRAWBERRY_VERSION_PATCH 7)
|
||||
set(STRAWBERRY_VERSION_PATCH 17)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
@@ -14,10 +14,6 @@ set(STRAWBERRY_VERSION_RPM_R "1")
|
||||
set(STRAWBERRY_VERSION_PAC_V "${majorminorpatch}")
|
||||
set(STRAWBERRY_VERSION_PAC_R "1")
|
||||
|
||||
if(${STRAWBERRY_VERSION_PATCH} EQUAL "0")
|
||||
set(STRAWBERRY_VERSION_DISPLAY "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}")
|
||||
endif(${STRAWBERRY_VERSION_PATCH} EQUAL "0")
|
||||
|
||||
if(STRAWBERRY_VERSION_PRERELEASE)
|
||||
set(STRAWBERRY_VERSION_DISPLAY "${STRAWBERRY_VERSION_DISPLAY} ${STRAWBERRY_VERSION_PRERELEASE}")
|
||||
set(STRAWBERRY_VERSION_RPM_R "0.${STRAWBERRY_VERSION_PRERELEASE}")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<file>schema/schema-13.sql</file>
|
||||
<file>schema/schema-14.sql</file>
|
||||
<file>schema/schema-15.sql</file>
|
||||
<file>schema/schema-16.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
|
||||
@@ -67,7 +67,21 @@ CREATE TABLE device_%deviceid_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -80,4 +94,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
);
|
||||
|
||||
UPDATE devices SET schema_version=3 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=4 WHERE ROWID=%deviceid;
|
||||
|
||||
217
data/schema/schema-16.sql
Normal file
217
data/schema/schema-16.sql
Normal file
@@ -0,0 +1,217 @@
|
||||
ALTER TABLE songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN acoustid_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN acoustid_fingerprint TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_album_artist_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_original_artist_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_album_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_original_album_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_track_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_disc_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
|
||||
ALTER TABLE playlist_items ADD COLUMN musicbrainz_work_id TEXT;
|
||||
|
||||
UPDATE schema_version SET version=16;
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (15);
|
||||
INSERT INTO schema_version (version) VALUES (16);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@@ -75,7 +75,21 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -137,7 +151,21 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -199,7 +227,21 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -261,7 +303,21 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -323,7 +379,21 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -385,7 +455,21 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -447,7 +531,21 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -509,7 +607,21 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -591,7 +703,21 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
rating INTEGER DEFAULT -1,
|
||||
|
||||
acoustid_id TEXT,
|
||||
acoustid_fingerprint TEXT,
|
||||
|
||||
musicbrainz_album_artist_id TEXT,
|
||||
musicbrainz_artist_id TEXT,
|
||||
musicbrainz_original_artist_id TEXT,
|
||||
musicbrainz_album_id TEXT,
|
||||
musicbrainz_original_album_id TEXT,
|
||||
musicbrainz_recording_id TEXT,
|
||||
musicbrainz_track_id TEXT,
|
||||
musicbrainz_disc_id TEXT,
|
||||
musicbrainz_release_group_id TEXT,
|
||||
musicbrainz_work_id TEXT
|
||||
|
||||
);
|
||||
|
||||
|
||||
3
debian/control.in
vendored
3
debian/control.in
vendored
@@ -17,6 +17,7 @@ Build-Depends: debhelper (>= 11),
|
||||
libasound2-dev,
|
||||
libpulse-dev,
|
||||
libtag1-dev,
|
||||
libicu-dev,
|
||||
@DEBIAN_BUILD_DEPENDS_QT_PACKAGES@,
|
||||
libgstreamer1.0-dev,
|
||||
libgstreamer-plugins-base1.0-dev,
|
||||
@@ -51,7 +52,7 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
|
||||
20
debian/copyright
vendored
20
debian/copyright
vendored
@@ -5,10 +5,10 @@ Source: https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2015, David Sansome <me@davidsansome.com>
|
||||
2012-2014, 2017-2022 Jonas Kvinge <jonas@jkvinge.net>
|
||||
2012-2014, 2017-2023 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/core/timeconstants.h
|
||||
Files: src/utilities/timeconstants.h
|
||||
ext/libstrawberry-common/core/logging.cpp
|
||||
ext/libstrawberry-common/core/logging.h
|
||||
ext/libstrawberry-common/core/messagehandler.cpp
|
||||
@@ -98,7 +98,7 @@ Files: src/core/main.h
|
||||
ext/macdeploycheck/*
|
||||
src/widgets/resizabletextedit.cpp
|
||||
src/widgets/resizabletextedit.h
|
||||
Copyright: 2012-2014, 2017-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
Copyright: 2012-2014, 2017-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/engine/enginebase.cpp
|
||||
@@ -130,11 +130,6 @@ Copyright: 2012, David Sansome <me@davidsansome.com>
|
||||
2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/core/appearance.cpp
|
||||
src/core/appearance.h
|
||||
Copyright: 2012, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/covermanager/discogscoverprovider.cpp
|
||||
src/covermanager/discogscoverprovider.h
|
||||
Copyright: 2012, Martin Björklund <mbj4668@gmail.com>
|
||||
@@ -232,9 +227,14 @@ Files: src/widgets/clickablelabel.cpp
|
||||
Copyright: 2010, 2011, Andrea Decorte <adecorte@gmail.com>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/widgets/volumeslider.cpp
|
||||
Files: src/widgets/sliderslider.cpp
|
||||
src/widgets/sliderslider.h
|
||||
src/widgets/prettyslider.cpp
|
||||
src/widgets/prettyslider.h
|
||||
src/widgets/volumeslider.cpp
|
||||
src/widgets/volumeslider.h
|
||||
Copyright: 2005, Gábor Lehel
|
||||
Copyright: 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
2005, Gábor Lehel
|
||||
2003, Mark Kretschmann <markey@web.de>
|
||||
License: GPL-2+
|
||||
|
||||
|
||||
2
dist/scripts/maketarball.sh.in
vendored
2
dist/scripts/maketarball.sh.in
vendored
@@ -49,6 +49,7 @@ ${TAR} -cJf $name-$version.tar.xz \
|
||||
--exclude="*.nsi" \
|
||||
--exclude="*.kdev4" \
|
||||
--exclude=".vscode" \
|
||||
--exclude=".idea" \
|
||||
--exclude="$root/.github" \
|
||||
--exclude="$root/.travis.yml" \
|
||||
--exclude="$root/.circleci" \
|
||||
@@ -56,6 +57,7 @@ ${TAR} -cJf $name-$version.tar.xz \
|
||||
--exclude="$root/CMakeLists.txt.user" \
|
||||
--exclude="$root/.clang-format" \
|
||||
--exclude="$root/build" \
|
||||
--exclude="$root/cmake-build-debug" \
|
||||
--exclude="$root/zanata.xml" \
|
||||
--exclude="$root/.zanata-cache" \
|
||||
--exclude="$root/debian/changelog" \
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<li>Edit tags on audio files</li>
|
||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
|
||||
<li>Support for multiple backends</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||
|
||||
@@ -9,6 +9,7 @@ TryExec=strawberry
|
||||
Icon=strawberry
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
Keywords=Audio;Player;
|
||||
StartupNotify=false
|
||||
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
||||
StartupWMClass=strawberry
|
||||
|
||||
2
dist/unix/strawberry.1
vendored
2
dist/unix/strawberry.1
vendored
@@ -29,7 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
|
||||
4
dist/unix/strawberry.spec.in
vendored
4
dist/unix/strawberry.spec.in
vendored
@@ -49,6 +49,8 @@ BuildRequires: pkgconfig(sqlite3) >= 3.9
|
||||
BuildRequires: pkgconfig(taglib)
|
||||
%endif
|
||||
BuildRequires: pkgconfig(fftw3)
|
||||
BuildRequires: pkgconfig(icu-uc)
|
||||
BuildRequires: pkgconfig(icu-i18n)
|
||||
%if "@QT_VERSION_MAJOR@" == "5" && ( 0%{?fedora} || 0%{?rhel_version} || 0%{?centos} )
|
||||
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Core)
|
||||
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Gui)
|
||||
@@ -117,7 +119,7 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
|
||||
272
dist/windows/strawberry.nsi.in
vendored
272
dist/windows/strawberry.nsi.in
vendored
@@ -1,53 +1,68 @@
|
||||
!define build_type ""
|
||||
!define compiler "unknown"
|
||||
!define arch "unknown"
|
||||
|
||||
!if "@ARCH@" == "x86"
|
||||
!define arch_x86
|
||||
!undef arch
|
||||
!define arch "x86"
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == "i686-w64-mingw32.shared"
|
||||
!define arch_x86
|
||||
!undef arch
|
||||
!define arch "x86"
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == "x86_64"
|
||||
!define arch_x64
|
||||
!undef arch
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||
!define arch_x64
|
||||
!undef arch
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
!if "@MINGW@" == "1"
|
||||
!define mingw
|
||||
!undef compiler
|
||||
!define compiler "mingw"
|
||||
!endif
|
||||
|
||||
!if "@MSVC@" == "1"
|
||||
!define msvc
|
||||
!undef compiler
|
||||
!define compiler "msvc"
|
||||
!endif
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||
!define release
|
||||
|
||||
!if "@ARCH@" == "x86"
|
||||
!define arch_x86
|
||||
!else if "@ARCH@" == "i686"
|
||||
!define arch_x86
|
||||
!else if "@ARCH@" == "i686-w64-mingw32.shared"
|
||||
!define arch_x86
|
||||
!else if "@ARCH@" == "x64"
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "x86_64"
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||
!define arch_x64
|
||||
!endif
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||
!ifdef arch_x86
|
||||
!define arch "x86"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x64
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||
!define release
|
||||
!else if "@CMAKE_BUILD_TYPE@" == "RelWithDebInfo"
|
||||
!define release
|
||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||
!define debug
|
||||
!undef build_type
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
!define build_type ""
|
||||
!endif
|
||||
|
||||
!ifdef debug
|
||||
!define build_type "-Debug"
|
||||
!endif
|
||||
|
||||
|
||||
!ifndef compiler
|
||||
!error "Missing compiler."
|
||||
!endif
|
||||
|
||||
!ifndef build_type
|
||||
!error "Missing build type."
|
||||
!endif
|
||||
|
||||
!ifndef arch
|
||||
!error "Missing arch."
|
||||
!endif
|
||||
|
||||
|
||||
|
||||
!ifdef debug
|
||||
!define PRODUCT_NAME "Strawberry Music Player Debug"
|
||||
!define PRODUCT_NAME_SHORT "Strawberry"
|
||||
@@ -128,6 +143,9 @@ SetCompressor /SOLID lzma
|
||||
!insertmacro MUI_PAGE_LICENSE COPYING
|
||||
Page Custom LockedListPageShow
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!ifdef msvc
|
||||
Page Custom InstallMSVCRuntime
|
||||
!endif
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
@@ -182,6 +200,20 @@ done:
|
||||
|
||||
FunctionEnd
|
||||
|
||||
!ifdef msvc
|
||||
!define vc_redist_file "vc_redist.${arch}.exe"
|
||||
Function InstallMSVCRuntime
|
||||
${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
|
||||
${If} $R0 == ""
|
||||
SetDetailsView hide
|
||||
inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
|
||||
Delete "$TEMP\${vc_redist_file}"
|
||||
SetDetailsView show
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
!endif
|
||||
|
||||
Function .onInit
|
||||
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
@@ -220,11 +252,11 @@ Section "Strawberry" Strawberry
|
||||
File "libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
File "avcodec-59.dll"
|
||||
File "avfilter-8.dll"
|
||||
File "avformat-59.dll"
|
||||
File "avutil-57.dll"
|
||||
File "libFLAC-8.dll"
|
||||
File "avcodec-60.dll"
|
||||
File "avfilter-9.dll"
|
||||
File "avformat-60.dll"
|
||||
File "avutil-58.dll"
|
||||
File "libFLAC-12.dll"
|
||||
File "libbrotlicommon.dll"
|
||||
File "libbrotlidec.dll"
|
||||
File "libbrotlienc.dll"
|
||||
@@ -240,6 +272,7 @@ Section "Strawberry" Strawberry
|
||||
File "libgcrypt-20.dll"
|
||||
File "libgio-2.0-0.dll"
|
||||
File "libglib-2.0-0.dll"
|
||||
File "libgme.dll"
|
||||
File "libgmodule-2.0-0.dll"
|
||||
File "libgmp-10.dll"
|
||||
File "libgnutls-30.dll"
|
||||
@@ -279,7 +312,6 @@ Section "Strawberry" Strawberry
|
||||
File "libopus-0.dll"
|
||||
File "liborc-0.4-0.dll"
|
||||
File "libpng16-16.dll"
|
||||
File "libprotobuf-32.dll"
|
||||
File "libpsl-5.dll"
|
||||
File "libqtsparkle-qt6.dll"
|
||||
File "libsoup-3.0-0.dll"
|
||||
@@ -290,7 +322,7 @@ Section "Strawberry" Strawberry
|
||||
File "libtag.dll"
|
||||
File "libtasn1-6.dll"
|
||||
File "libtwolame-0.dll"
|
||||
File "libunistring-2.dll"
|
||||
File "libunistring-5.dll"
|
||||
File "libvorbis-0.dll"
|
||||
File "libvorbisenc-2.dll"
|
||||
File "libvorbisfile-3.dll"
|
||||
@@ -298,11 +330,51 @@ Section "Strawberry" Strawberry
|
||||
File "libwinpthread-1.dll"
|
||||
File "libxml2-2.dll"
|
||||
File "libzstd.dll"
|
||||
File "postproc-56.dll"
|
||||
File "postproc-57.dll"
|
||||
File "swresample-4.dll"
|
||||
File "swscale-6.dll"
|
||||
File "swscale-7.dll"
|
||||
File "zlib1.dll"
|
||||
|
||||
File "libabsl_base.dll"
|
||||
File "libabsl_city.dll"
|
||||
File "libabsl_cord.dll"
|
||||
File "libabsl_cord_internal.dll"
|
||||
File "libabsl_cordz_handle.dll"
|
||||
File "libabsl_cordz_info.dll"
|
||||
File "libabsl_crc32c.dll"
|
||||
File "libabsl_crc_cord_state.dll"
|
||||
File "libabsl_crc_internal.dll"
|
||||
File "libabsl_die_if_null.dll"
|
||||
File "libabsl_examine_stack.dll"
|
||||
File "libabsl_hash.dll"
|
||||
File "libabsl_int128.dll"
|
||||
File "libabsl_log_globals.dll"
|
||||
File "libabsl_log_internal_check_op.dll"
|
||||
File "libabsl_log_internal_format.dll"
|
||||
File "libabsl_log_internal_globals.dll"
|
||||
File "libabsl_log_internal_log_sink_set.dll"
|
||||
File "libabsl_log_internal_message.dll"
|
||||
File "libabsl_log_internal_nullguard.dll"
|
||||
File "libabsl_log_internal_proto.dll"
|
||||
File "libabsl_log_sink.dll"
|
||||
File "libabsl_low_level_hash.dll"
|
||||
File "libabsl_malloc_internal.dll"
|
||||
File "libabsl_raw_hash_set.dll"
|
||||
File "libabsl_raw_logging_internal.dll"
|
||||
File "libabsl_spinlock_wait.dll"
|
||||
File "libabsl_stacktrace.dll"
|
||||
File "libabsl_status.dll"
|
||||
File "libabsl_statusor.dll"
|
||||
File "libabsl_strerror.dll"
|
||||
File "libabsl_str_format_internal.dll"
|
||||
File "libabsl_strings.dll"
|
||||
File "libabsl_strings_internal.dll"
|
||||
File "libabsl_symbolize.dll"
|
||||
File "libabsl_synchronization.dll"
|
||||
File "libabsl_throw_delegate.dll"
|
||||
File "libabsl_time.dll"
|
||||
File "libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
File "gdb.exe"
|
||||
File "libexpat-1.dll"
|
||||
@@ -312,6 +384,7 @@ Section "Strawberry" Strawberry
|
||||
File "libpcre2-16d.dll"
|
||||
File "libreadline8.dll"
|
||||
File "libtermcap.dll"
|
||||
File "libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
File "libpcre2-8.dll"
|
||||
File "libpcre2-16.dll"
|
||||
@@ -336,7 +409,6 @@ Section "Strawberry" Strawberry
|
||||
File "avcodec-58.dll"
|
||||
File "avfilter-7.dll"
|
||||
File "avformat-58.dll"
|
||||
File "avresample-4.dll"
|
||||
File "avutil-56.dll"
|
||||
File "brotlicommon.dll"
|
||||
File "brotlidec.dll"
|
||||
@@ -346,6 +418,7 @@ Section "Strawberry" Strawberry
|
||||
File "ffi-7.dll"
|
||||
File "gio-2.0-0.dll"
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
File "gnutls.dll"
|
||||
File "gobject-2.0-0.dll"
|
||||
@@ -366,23 +439,25 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "libbs2b.dll"
|
||||
File "libfaac_dll.dll"
|
||||
File "libiconv.dll"
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "libspeex.dll"
|
||||
File "mpcdec.dll"
|
||||
File "mpg123.dll"
|
||||
File "nghttp2.dll"
|
||||
File "ogg.dll"
|
||||
File "opus.dll"
|
||||
File "orc-0.4-0.dll"
|
||||
File "postproc-55.dll"
|
||||
File "psl-5.dll"
|
||||
File "qtsparkle-qt6.dll"
|
||||
File "soup-2.4-1.dll"
|
||||
File "soup-3.0-0.dll"
|
||||
File "sqlite3.dll"
|
||||
File "swresample-3.dll"
|
||||
File "swscale-5.dll"
|
||||
@@ -390,10 +465,12 @@ Section "Strawberry" Strawberry
|
||||
File "vorbis.dll"
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
File "abseil_dll.dll"
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
File "libiconv.dll"
|
||||
File "libpng16.dll"
|
||||
File "libprotobuf.dll"
|
||||
File "libxml2.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
@@ -401,8 +478,9 @@ Section "Strawberry" Strawberry
|
||||
File "zlib.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
File "libiconvd.dll"
|
||||
File "libpng16d.dll"
|
||||
File "libprotobufd.dll"
|
||||
File "libxml2d.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
@@ -414,8 +492,16 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt72.dll"
|
||||
File "libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
File "libprotobufd.dll"
|
||||
!else
|
||||
File "libprotobuf.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin72d.dll"
|
||||
File "icuuc72d.dll"
|
||||
File "Qt6Concurrentd.dll"
|
||||
File "Qt6Cored.dll"
|
||||
File "Qt6Guid.dll"
|
||||
@@ -423,6 +509,8 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Sqld.dll"
|
||||
File "Qt6Widgetsd.dll"
|
||||
!else
|
||||
File "icuin72.dll"
|
||||
File "icuuc72.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
File "Qt6Gui.dll"
|
||||
@@ -554,6 +642,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstfdkaac.dll" "gstreamer-plugins\libgstfdkaac.dll"
|
||||
File "/oname=libgstflac.dll" "gstreamer-plugins\libgstflac.dll"
|
||||
File "/oname=libgstgio.dll" "gstreamer-plugins\libgstgio.dll"
|
||||
File "/oname=libgstgme.dll" "gstreamer-plugins\libgstgme.dll"
|
||||
File "/oname=libgsthls.dll" "gstreamer-plugins\libgsthls.dll"
|
||||
File "/oname=libgsticydemux.dll" "gstreamer-plugins\libgsticydemux.dll"
|
||||
File "/oname=libgstid3demux.dll" "gstreamer-plugins\libgstid3demux.dll"
|
||||
@@ -614,6 +703,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
||||
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
|
||||
File "/oname=gstgio.dll" "gstreamer-plugins\gstgio.dll"
|
||||
File "/oname=gstgme.dll" "gstreamer-plugins\gstgme.dll"
|
||||
File "/oname=gsthls.dll" "gstreamer-plugins\gsthls.dll"
|
||||
File "/oname=gsticydemux.dll" "gstreamer-plugins\gsticydemux.dll"
|
||||
File "/oname=gstid3demux.dll" "gstreamer-plugins\gstid3demux.dll"
|
||||
@@ -709,11 +799,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\avcodec-59.dll"
|
||||
Delete "$INSTDIR\avfilter-8.dll"
|
||||
Delete "$INSTDIR\avformat-59.dll"
|
||||
Delete "$INSTDIR\avutil-57.dll"
|
||||
Delete "$INSTDIR\libFLAC-8.dll"
|
||||
Delete "$INSTDIR\avcodec-60.dll"
|
||||
Delete "$INSTDIR\avfilter-9.dll"
|
||||
Delete "$INSTDIR\avformat-60.dll"
|
||||
Delete "$INSTDIR\avutil-58.dll"
|
||||
Delete "$INSTDIR\libFLAC-12.dll"
|
||||
Delete "$INSTDIR\libbrotlicommon.dll"
|
||||
Delete "$INSTDIR\libbrotlidec.dll"
|
||||
Delete "$INSTDIR\libbrotlienc.dll"
|
||||
@@ -729,6 +819,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libgcrypt-20.dll"
|
||||
Delete "$INSTDIR\libgio-2.0-0.dll"
|
||||
Delete "$INSTDIR\libglib-2.0-0.dll"
|
||||
Delete "$INSTDIR\libgme.dll"
|
||||
Delete "$INSTDIR\libgmodule-2.0-0.dll"
|
||||
Delete "$INSTDIR\libgmp-10.dll"
|
||||
Delete "$INSTDIR\libgnutls-30.dll"
|
||||
@@ -768,7 +859,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libopus-0.dll"
|
||||
Delete "$INSTDIR\liborc-0.4-0.dll"
|
||||
Delete "$INSTDIR\libpng16-16.dll"
|
||||
Delete "$INSTDIR\libprotobuf-32.dll"
|
||||
Delete "$INSTDIR\libpsl-5.dll"
|
||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||
@@ -779,7 +869,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libtag.dll"
|
||||
Delete "$INSTDIR\libtasn1-6.dll"
|
||||
Delete "$INSTDIR\libtwolame-0.dll"
|
||||
Delete "$INSTDIR\libunistring-2.dll"
|
||||
Delete "$INSTDIR\libunistring-5.dll"
|
||||
Delete "$INSTDIR\libvorbis-0.dll"
|
||||
Delete "$INSTDIR\libvorbisenc-2.dll"
|
||||
Delete "$INSTDIR\libvorbisfile-3.dll"
|
||||
@@ -787,11 +877,51 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libwinpthread-1.dll"
|
||||
Delete "$INSTDIR\libxml2-2.dll"
|
||||
Delete "$INSTDIR\libzstd.dll"
|
||||
Delete "$INSTDIR\postproc-56.dll"
|
||||
Delete "$INSTDIR\postproc-57.dll"
|
||||
Delete "$INSTDIR\swresample-4.dll"
|
||||
Delete "$INSTDIR\swscale-6.dll"
|
||||
Delete "$INSTDIR\swscale-7.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
|
||||
Delete "$INSTDIR\libabsl_base.dll"
|
||||
Delete "$INSTDIR\libabsl_city.dll"
|
||||
Delete "$INSTDIR\libabsl_cord.dll"
|
||||
Delete "$INSTDIR\libabsl_cord_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_handle.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_info.dll"
|
||||
Delete "$INSTDIR\libabsl_crc32c.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_cord_state.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_die_if_null.dll"
|
||||
Delete "$INSTDIR\libabsl_examine_stack.dll"
|
||||
Delete "$INSTDIR\libabsl_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_int128.dll"
|
||||
Delete "$INSTDIR\libabsl_log_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_format.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_log_sink_set.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_message.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_nullguard.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_proto.dll"
|
||||
Delete "$INSTDIR\libabsl_log_sink.dll"
|
||||
Delete "$INSTDIR\libabsl_low_level_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_malloc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_hash_set.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_logging_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_spinlock_wait.dll"
|
||||
Delete "$INSTDIR\libabsl_stacktrace.dll"
|
||||
Delete "$INSTDIR\libabsl_status.dll"
|
||||
Delete "$INSTDIR\libabsl_statusor.dll"
|
||||
Delete "$INSTDIR\libabsl_strerror.dll"
|
||||
Delete "$INSTDIR\libabsl_str_format_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_strings.dll"
|
||||
Delete "$INSTDIR\libabsl_strings_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_symbolize.dll"
|
||||
Delete "$INSTDIR\libabsl_synchronization.dll"
|
||||
Delete "$INSTDIR\libabsl_throw_delegate.dll"
|
||||
Delete "$INSTDIR\libabsl_time.dll"
|
||||
Delete "$INSTDIR\libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\gdb.exe"
|
||||
Delete "$INSTDIR\libexpat-1.dll"
|
||||
@@ -801,6 +931,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libpcre2-16d.dll"
|
||||
Delete "$INSTDIR\libreadline8.dll"
|
||||
Delete "$INSTDIR\libtermcap.dll"
|
||||
Delete "$INSTDIR\libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libpcre2-8.dll"
|
||||
Delete "$INSTDIR\libpcre2-16.dll"
|
||||
@@ -825,7 +956,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\avcodec-58.dll"
|
||||
Delete "$INSTDIR\avfilter-7.dll"
|
||||
Delete "$INSTDIR\avformat-58.dll"
|
||||
Delete "$INSTDIR\avresample-4.dll"
|
||||
Delete "$INSTDIR\avutil-56.dll"
|
||||
Delete "$INSTDIR\brotlicommon.dll"
|
||||
Delete "$INSTDIR\brotlidec.dll"
|
||||
@@ -835,6 +965,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\ffi-7.dll"
|
||||
Delete "$INSTDIR\gio-2.0-0.dll"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
@@ -855,23 +986,25 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
Delete "$INSTDIR\libiconv.dll"
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
Delete "$INSTDIR\mpg123.dll"
|
||||
Delete "$INSTDIR\nghttp2.dll"
|
||||
Delete "$INSTDIR\ogg.dll"
|
||||
Delete "$INSTDIR\opus.dll"
|
||||
Delete "$INSTDIR\orc-0.4-0.dll"
|
||||
Delete "$INSTDIR\postproc-55.dll"
|
||||
Delete "$INSTDIR\psl-5.dll"
|
||||
Delete "$INSTDIR\qtsparkle-qt6.dll"
|
||||
Delete "$INSTDIR\soup-2.4-1.dll"
|
||||
Delete "$INSTDIR\soup-3.0-0.dll"
|
||||
Delete "$INSTDIR\sqlite3.dll"
|
||||
Delete "$INSTDIR\swresample-3.dll"
|
||||
Delete "$INSTDIR\swscale-5.dll"
|
||||
@@ -879,10 +1012,12 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbis.dll"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
Delete "$INSTDIR\abseil_dll.dll"
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
Delete "$INSTDIR\libiconv.dll"
|
||||
Delete "$INSTDIR\libpng16.dll"
|
||||
Delete "$INSTDIR\libprotobuf.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
@@ -890,8 +1025,9 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\zlib.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
Delete "$INSTDIR\libiconvd.dll"
|
||||
Delete "$INSTDIR\libpng16d.dll"
|
||||
Delete "$INSTDIR\libprotobufd.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
@@ -903,8 +1039,16 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt72.dll"
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\libprotobufd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libprotobuf.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin72d.dll"
|
||||
Delete "$INSTDIR\icuuc72d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
Delete "$INSTDIR\Qt6Guid.dll"
|
||||
@@ -912,6 +1056,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin72.dll"
|
||||
Delete "$INSTDIR\icuuc72.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
Delete "$INSTDIR\Qt6Gui.dll"
|
||||
@@ -976,6 +1122,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstfdkaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstflac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstgio.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstgme.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgsthls.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgsticydemux.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstid3demux.dll"
|
||||
@@ -1038,6 +1185,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstgio.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstgme.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsthls.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsticydemux.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstid3demux.dll"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(SOURCES gstfastspectrum.cpp gstmoodbarplugin.cpp)
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ static void gst_fastspectrum_class_init(GstFastSpectrumClass *klass) {
|
||||
|
||||
filter_class->setup = GST_DEBUG_FUNCPTR(gst_fastspectrum_setup);
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_INTERVAL, g_param_spec_uint64("interval", "Interval", "Interval of time between message posts (in nanoseconds)", 1, G_MAXUINT64, DEFAULT_INTERVAL, GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
g_object_class_install_property(gobject_class, PROP_INTERVAL, g_param_spec_uint64("interval", "Interval", "Interval of time between message posts (in nanoseconds)", 1, G_MAXUINT64, DEFAULT_INTERVAL, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_BANDS, g_param_spec_uint("bands", "Bands", "Number of frequency bands", 0, G_MAXUINT, DEFAULT_BANDS, GParamFlags(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
g_object_class_install_property(gobject_class, PROP_BANDS, g_param_spec_uint("bands", "Bands", "Number of frequency bands", 0, G_MAXUINT, DEFAULT_BANDS, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT(gst_fastspectrum_debug, "spectrum", 0, "audio spectrum analyser element");
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class QMutex;
|
||||
|
||||
typedef void (*GstFastSpectrumInputData)(const guint8 *in, double *out, guint len, double max_value, guint op, guint nfft);
|
||||
|
||||
typedef std::function<void(double *magnitudes, int size)> OutputCallback;
|
||||
using OutputCallback = std::function<void(double *magnitudes, int size)>;
|
||||
|
||||
struct GstFastSpectrum {
|
||||
GstAudioFilter parent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(SOURCES
|
||||
core/logging.cpp
|
||||
|
||||
@@ -126,7 +126,8 @@ class LoggedDebug : public DebugBase<LoggedDebug> {
|
||||
static void MessageHandler(QtMsgType type, const QMessageLogContext&, const QString &message) {
|
||||
|
||||
if (message.startsWith(kMessageHandlerMagic)) {
|
||||
fprintf(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout, "%s\n", message.toUtf8().data() + kMessageHandlerMagicLength);
|
||||
QByteArray message_data = message.toUtf8();
|
||||
fprintf(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout, "%s\n", message_data.constData() + kMessageHandlerMagicLength);
|
||||
fflush(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
|
||||
class QIODevice;
|
||||
|
||||
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
|
||||
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
|
||||
|
||||
// Reads and writes uint32 length encoded protobufs to a socket.
|
||||
// This base QObject is separate from AbstractMessageHandler because moc can't handle templated classes.
|
||||
// Use AbstractMessageHandler instead.
|
||||
@@ -85,8 +82,8 @@ class AbstractMessageHandler : public _MessageHandlerBase {
|
||||
AbstractMessageHandler(QIODevice *device, QObject *parent);
|
||||
~AbstractMessageHandler() override { AbstractMessageHandler::AbortAll(); }
|
||||
|
||||
typedef MT MessageType;
|
||||
typedef MessageReply<MT> ReplyType;
|
||||
using MessageType = MT;
|
||||
using ReplyType = MessageReply<MT>;
|
||||
|
||||
// Serialises the message and writes it to the socket.
|
||||
// This version MUST be called from the thread in which the AbstractMessageHandler was created.
|
||||
|
||||
@@ -76,8 +76,8 @@ class WorkerPool : public _WorkerPoolBase {
|
||||
explicit WorkerPool(QObject *parent = nullptr);
|
||||
~WorkerPool() override;
|
||||
|
||||
typedef typename HandlerType::MessageType MessageType;
|
||||
typedef typename HandlerType::ReplyType ReplyType;
|
||||
using MessageType = typename HandlerType::MessageType;
|
||||
using ReplyType = typename HandlerType::ReplyType;
|
||||
|
||||
// Sets the name of the worker executable. This is looked for first in the current directory, and then in $PATH.
|
||||
// You must call this before calling Start().
|
||||
@@ -165,10 +165,10 @@ class WorkerPool : public _WorkerPoolBase {
|
||||
template<typename HandlerType>
|
||||
WorkerPool<HandlerType>::WorkerPool(QObject *parent)
|
||||
: _WorkerPoolBase(parent),
|
||||
worker_count_(1),
|
||||
next_worker_(0),
|
||||
next_id_(0) {
|
||||
|
||||
worker_count_ = qBound(1, QThread::idealThreadCount() / 2, 4);
|
||||
local_server_name_ = qApp->applicationName().toLower();
|
||||
|
||||
if (local_server_name_.isEmpty()) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(MESSAGES tagreadermessages.proto)
|
||||
set(SOURCES tagreaderbase.cpp)
|
||||
|
||||
if(USE_TAGLIB AND TAGLIB_FOUND)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp)
|
||||
endif()
|
||||
|
||||
if(USE_TAGPARSER AND TAGPARSER_FOUND)
|
||||
@@ -43,12 +43,21 @@ target_include_directories(libstrawberry-tagreader PRIVATE
|
||||
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
${PROTOBUF_LIBRARY}
|
||||
${Protobuf_LIBRARIES}
|
||||
${QtCore_LIBRARIES}
|
||||
${QtNetwork_LIBRARIES}
|
||||
${QtGui_LIBRARIES}
|
||||
libstrawberry-common
|
||||
)
|
||||
|
||||
if(WIN32 AND Protobuf_VERSION VERSION_GREATER_EQUAL 4.22.0)
|
||||
if (MSVC)
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE abseil_dll)
|
||||
else()
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE absl_log_internal_message absl_log_internal_check_op)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_TAGLIB AND TAGLIB_FOUND)
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QBuffer>
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tagreaderbase.h"
|
||||
|
||||
const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
|
||||
@@ -49,3 +58,86 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) {
|
||||
return 0xFF;
|
||||
|
||||
}
|
||||
|
||||
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request) {
|
||||
|
||||
if (!request.has_save_cover() || !request.save_cover()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
|
||||
}
|
||||
bool cover_is_jpeg = false;
|
||||
if (request.has_cover_is_jpeg()) {
|
||||
cover_is_jpeg = request.cover_is_jpeg();
|
||||
}
|
||||
|
||||
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
|
||||
|
||||
}
|
||||
|
||||
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) {
|
||||
|
||||
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
|
||||
}
|
||||
bool cover_is_jpeg = false;
|
||||
if (request.has_cover_is_jpeg()) {
|
||||
cover_is_jpeg = request.cover_is_jpeg();
|
||||
}
|
||||
|
||||
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
|
||||
|
||||
}
|
||||
|
||||
QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg) {
|
||||
|
||||
if (!cover_data.isEmpty() && cover_is_jpeg) {
|
||||
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
|
||||
return cover_data;
|
||||
}
|
||||
|
||||
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
|
||||
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
|
||||
QFile file(cover_filename);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
|
||||
return QByteArray();
|
||||
}
|
||||
cover_data = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
|
||||
if (!cover_data.isEmpty()) {
|
||||
if (QMimeDatabase().mimeTypeForData(cover_data).name() == "image/jpeg") {
|
||||
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
|
||||
return cover_data;
|
||||
}
|
||||
// Convert image to JPEG.
|
||||
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
|
||||
QImage cover_image(cover_data);
|
||||
cover_data.clear();
|
||||
QBuffer buffer(&cover_data);
|
||||
if (buffer.open(QIODevice::WriteOnly)) {
|
||||
cover_image.save(&buffer, "JPEG");
|
||||
buffer.close();
|
||||
}
|
||||
return cover_data;
|
||||
}
|
||||
|
||||
return QByteArray();
|
||||
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ class TagReaderBase {
|
||||
|
||||
virtual bool IsMediaFile(const QString &filename) const = 0;
|
||||
|
||||
virtual void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual bool SaveFile(const spb::tagreader::SaveFileRequest &request) const = 0;
|
||||
|
||||
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
|
||||
virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0;
|
||||
virtual bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
|
||||
|
||||
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
@@ -50,6 +50,12 @@ class TagReaderBase {
|
||||
static float ConvertPOPMRating(const int POPM_rating);
|
||||
static int ConvertToPOPMRating(const float rating);
|
||||
|
||||
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request);
|
||||
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request);
|
||||
|
||||
private:
|
||||
static QByteArray LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg);
|
||||
|
||||
protected:
|
||||
static const std::string kEmbeddedCover;
|
||||
|
||||
|
||||
300
ext/libstrawberry-tagreader/tagreadergme.cpp
Normal file
300
ext/libstrawberry-tagreader/tagreadergme.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2022, Eoin O'Neill <eoinoneill1991@gmail.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "tagreadergme.h"
|
||||
|
||||
#include <tag.h>
|
||||
#include <apefile.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QChar>
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadertaglib.h"
|
||||
|
||||
bool GME::IsSupportedFormat(const QFileInfo &file_info) {
|
||||
return file_info.exists() && (file_info.completeSuffix().endsWith("spc", Qt::CaseInsensitive) || file_info.completeSuffix().endsWith("vgm"), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool GME::ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
if (file_info.completeSuffix().endsWith("spc"), Qt::CaseInsensitive) {
|
||||
SPC::Read(file_info, song_info);
|
||||
return true;
|
||||
}
|
||||
if (file_info.completeSuffix().endsWith("vgm", Qt::CaseInsensitive)) {
|
||||
VGM::Read(file_info, song_info);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
quint32 GME::UnpackBytes32(const char *const bytes, size_t length) {
|
||||
|
||||
Q_ASSERT(length <= 4 && length > 0);
|
||||
|
||||
quint32 value = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
value |= static_cast<unsigned char>(bytes[i]) << (8 * i);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
}
|
||||
|
||||
void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
QFile file(file_info.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) return;
|
||||
|
||||
qLog(Debug) << "Reading tags from SPC file" << file_info.fileName();
|
||||
|
||||
// Check for header -- more reliable than file name alone.
|
||||
if (!file.read(33).startsWith(QString("SNES-SPC700").toLatin1())) return;
|
||||
|
||||
// First order of business -- get any tag values that exist within the core file information.
|
||||
// These only allow for a certain number of bytes per field,
|
||||
// so they will likely be overwritten either by the id666 standard or the APETAG format (as used by other players, such as foobar and winamp)
|
||||
//
|
||||
// Make sure to check id6 documentation before changing the read values!
|
||||
|
||||
file.seek(HAS_ID6_OFFSET);
|
||||
const QByteArray id6_status = file.read(1);
|
||||
const bool has_id6 = id6_status.length() >= 1 && id6_status[0] == static_cast<char>(xID6_STATUS::ON);
|
||||
|
||||
file.seek(SONG_TITLE_OFFSET);
|
||||
song_info->set_title(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(GAME_TITLE_OFFSET);
|
||||
song_info->set_album(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(ARTIST_OFFSET);
|
||||
song_info->set_artist(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(INTRO_LENGTH_OFFSET);
|
||||
QByteArray length_bytes = file.read(INTRO_LENGTH_SIZE);
|
||||
quint64 length_in_sec = 0;
|
||||
if (length_bytes.size() >= INTRO_LENGTH_SIZE) {
|
||||
length_in_sec = ConvertSPCStringToNum(length_bytes);
|
||||
|
||||
if (!length_in_sec || length_in_sec >= 0x1FFF) {
|
||||
// This means that parsing the length as a string failed, so get value LE.
|
||||
length_in_sec = length_bytes[0] | (length_bytes[1] << 8) | (length_bytes[2] << 16);
|
||||
}
|
||||
|
||||
if (length_in_sec < 0x1FFF) {
|
||||
song_info->set_length_nanosec(length_in_sec * kNsecPerSec);
|
||||
}
|
||||
}
|
||||
|
||||
file.seek(FADE_LENGTH_OFFSET);
|
||||
QByteArray fade_bytes = file.read(FADE_LENGTH_SIZE);
|
||||
if (fade_bytes.size() >= FADE_LENGTH_SIZE) {
|
||||
quint64 fade_length_in_ms = ConvertSPCStringToNum(fade_bytes);
|
||||
|
||||
if (fade_length_in_ms > 0x7FFF) {
|
||||
fade_length_in_ms = fade_bytes[0] | (fade_bytes[1] << 8) | (fade_bytes[2] << 16) | (fade_bytes[3] << 24);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for XID6 data -- this is infrequently used, but being able to fill in data from this is ideal before trying to rely on APETAG values.
|
||||
// XID6 format follows EA's binary file format standard named "IFF"
|
||||
file.seek(XID6_OFFSET);
|
||||
if (has_id6 && file.read(4) == QString("xid6").toLatin1()) {
|
||||
QByteArray xid6_head_data = file.read(4);
|
||||
if (xid6_head_data.size() >= 4) {
|
||||
qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) | (xid6_head_data[2] << 16) | xid6_head_data[3];
|
||||
// This should be the size remaining for entire ID6 block, but it seems that most files treat this as the size of the remaining header space...
|
||||
|
||||
qLog(Debug) << file_info.fileName() << "has ID6 tag.";
|
||||
|
||||
while ((file.pos()) + 4 < XID6_OFFSET + xid6_size) {
|
||||
QByteArray arr = file.read(4);
|
||||
if (arr.size() < 4) break;
|
||||
|
||||
qint8 id = arr[0];
|
||||
qint8 type = arr[1];
|
||||
Q_UNUSED(id);
|
||||
Q_UNUSED(type);
|
||||
qint16 length = arr[2] | (arr[3] << 8);
|
||||
|
||||
file.read(GetNextMemAddressAlign32bit(length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Music Players that support SPC tend to support additional tagging data as
|
||||
// an APETAG entry at the bottom of the file instead of writing into the xid6 tagging space.
|
||||
// This is where a lot of the extra data for a file is stored, such as genre or replaygain data.
|
||||
// This data is currently supported by TagLib, so we will simply use that for the remaining values.
|
||||
TagLib::APE::File ape(file_info.filePath().toStdString().data());
|
||||
if (ape.hasAPETag()) {
|
||||
TagLib::Tag *tag = ape.tag();
|
||||
if (!tag) return;
|
||||
|
||||
TagReaderTagLib::TStringToStdString(tag->artist(), song_info->mutable_artist());
|
||||
TagReaderTagLib::TStringToStdString(tag->album(), song_info->mutable_album());
|
||||
TagReaderTagLib::TStringToStdString(tag->title(), song_info->mutable_title());
|
||||
TagReaderTagLib::TStringToStdString(tag->genre(), song_info->mutable_genre());
|
||||
song_info->set_track(tag->track());
|
||||
song_info->set_year(tag->year());
|
||||
}
|
||||
|
||||
song_info->set_valid(true);
|
||||
song_info->set_filetype(spb::tagreader::SongMetadata_FileType_SPC);
|
||||
|
||||
}
|
||||
|
||||
qint16 GME::SPC::GetNextMemAddressAlign32bit(qint16 input) {
|
||||
return ((input + 0x3) & ~0x3);
|
||||
// Plus 0x3 for rounding up (not down), AND NOT to flatten out on a 32 bit level.
|
||||
}
|
||||
|
||||
quint64 GME::SPC::ConvertSPCStringToNum(const QByteArray &arr) {
|
||||
|
||||
quint64 result = 0;
|
||||
for (auto it = arr.begin(); it != arr.end(); it++) {
|
||||
unsigned int num = *it - '0';
|
||||
if (num > 9) break;
|
||||
result = (result * 10) + num; // Shift Left and add.
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
void GME::VGM::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
QFile file(file_info.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) return;
|
||||
|
||||
qLog(Debug) << "Reading tags from VGM file" << file_info.fileName();
|
||||
|
||||
if (!file.read(4).startsWith(QString("Vgm ").toLatin1())) return;
|
||||
|
||||
file.seek(GD3_TAG_PTR);
|
||||
QByteArray gd3_head = file.read(4);
|
||||
if (gd3_head.size() < 4) return;
|
||||
|
||||
quint64 pt = GME::UnpackBytes32(gd3_head.constData(), gd3_head.size());
|
||||
|
||||
file.seek(SAMPLE_COUNT);
|
||||
QByteArray sample_count_bytes = file.read(4);
|
||||
file.seek(LOOP_SAMPLE_COUNT);
|
||||
QByteArray loop_count_bytes = file.read(4);
|
||||
quint64 length = 0;
|
||||
|
||||
if (!GetPlaybackLength(sample_count_bytes, loop_count_bytes, length)) return;
|
||||
|
||||
file.seek(GD3_TAG_PTR + pt);
|
||||
QByteArray gd3_version = file.read(4);
|
||||
|
||||
file.seek(file.pos() + 4);
|
||||
QByteArray gd3_length_bytes = file.read(4);
|
||||
quint32 gd3_length = GME::UnpackBytes32(gd3_length_bytes.constData(), gd3_length_bytes.size());
|
||||
|
||||
QByteArray gd3Data = file.read(gd3_length);
|
||||
QTextStream fileTagStream(gd3Data, QIODevice::ReadOnly);
|
||||
// Stored as 16 bit UTF string, two bytes per letter.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
fileTagStream.setEncoding(QStringConverter::Utf16);
|
||||
#else
|
||||
fileTagStream.setCodec("UTF-16");
|
||||
#endif
|
||||
QStringList strings = fileTagStream.readLine(0).split(QChar('\0'));
|
||||
if (strings.count() < 10) return;
|
||||
|
||||
// VGM standard dictates string tag data exist in specific order.
|
||||
// Order alternates between English and Japanese version of data.
|
||||
// Read GD3 tag standard for more details.
|
||||
song_info->set_title(strings[0].toStdString());
|
||||
song_info->set_album(strings[2].toStdString());
|
||||
song_info->set_artist(strings[6].toStdString());
|
||||
song_info->set_year(strings[8].left(4).toInt());
|
||||
song_info->set_length_nanosec(length * kNsecPerMsec);
|
||||
song_info->set_valid(true);
|
||||
song_info->set_filetype(spb::tagreader::SongMetadata_FileType_VGM);
|
||||
|
||||
}
|
||||
|
||||
bool GME::VGM::GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length) {
|
||||
|
||||
if (sample_count_bytes.size() != 4) return false;
|
||||
if (loop_count_bytes.size() != 4) return false;
|
||||
|
||||
quint64 sample_count = GME::UnpackBytes32(sample_count_bytes.constData(), sample_count_bytes.size());
|
||||
|
||||
if (sample_count <= 0) return false;
|
||||
|
||||
quint64 loop_sample_count = GME::UnpackBytes32(loop_count_bytes.constData(), loop_count_bytes.size());
|
||||
|
||||
if (loop_sample_count <= 0) {
|
||||
out_length = sample_count * 1000 / SAMPLE_TIMEBASE;
|
||||
return true;
|
||||
}
|
||||
|
||||
quint64 intro_length_ms = (sample_count - loop_sample_count) * 1000 / SAMPLE_TIMEBASE;
|
||||
quint64 loop_length_ms = (loop_sample_count) * 1000 / SAMPLE_TIMEBASE;
|
||||
out_length = intro_length_ms + (loop_length_ms * 2) + GST_GME_LOOP_TIME_MS;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
TagReaderGME::TagReaderGME() = default;
|
||||
TagReaderGME::~TagReaderGME() = default;
|
||||
|
||||
bool TagReaderGME::IsMediaFile(const QString &filename) const {
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::IsSupportedFormat(fileinfo);
|
||||
}
|
||||
|
||||
bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::ReadFile(fileinfo, song);
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveFile(const spb::tagreader::SaveFileRequest&) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest&) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveSongRatingToFile(const QString&, const spb::tagreader::SongMetadata&) const {
|
||||
return false;
|
||||
}
|
||||
119
ext/libstrawberry-tagreader/tagreadergme.h
Normal file
119
ext/libstrawberry-tagreader/tagreadergme.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2022, Eoin O'Neill <eoinoneill1991@gmail.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERGME_H
|
||||
#define TAGREADERGME_H
|
||||
|
||||
#include <taglib/tstring.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
|
||||
namespace GME {
|
||||
bool IsSupportedFormat(const QFileInfo &file_info);
|
||||
bool ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
|
||||
uint32_t UnpackBytes32(const char *const arr, size_t length);
|
||||
|
||||
namespace SPC {
|
||||
// SPC SPEC: http://vspcplay.raphnet.net/spc_file_format.txt
|
||||
|
||||
constexpr int HAS_ID6_OFFSET = 0x23;
|
||||
constexpr int SONG_TITLE_OFFSET = 0x2E;
|
||||
constexpr int GAME_TITLE_OFFSET = 0x4E;
|
||||
constexpr int DUMPER_OFFSET = 0x6E;
|
||||
constexpr int COMMENTS_OFFSET = 0x7E;
|
||||
// It seems that intro length and fade length are inconsistent from file to file.
|
||||
// It should be looked into within the GME source code to see how GStreamer gets its values for playback length.
|
||||
constexpr int INTRO_LENGTH_OFFSET = 0xA9;
|
||||
constexpr int INTRO_LENGTH_SIZE = 3;
|
||||
constexpr int FADE_LENGTH_OFFSET = 0xAC;
|
||||
constexpr int FADE_LENGTH_SIZE = 4;
|
||||
constexpr int ARTIST_OFFSET = 0xB1;
|
||||
constexpr int XID6_OFFSET = (0x101C0 + 64);
|
||||
|
||||
constexpr int NANO_PER_MS = 1000000;
|
||||
|
||||
enum class xID6_STATUS {
|
||||
ON = 0x26,
|
||||
OFF = 0x27
|
||||
};
|
||||
|
||||
enum class xID6_ID {
|
||||
SongName = 0x01,
|
||||
GameName = 0x02,
|
||||
ArtistName = 0x03
|
||||
};
|
||||
|
||||
enum class xID6_TYPE {
|
||||
Length = 0x0,
|
||||
String = 0x1,
|
||||
Integer = 0x4
|
||||
};
|
||||
|
||||
void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
qint16 GetNextMemAddressAlign32bit(qint16 input);
|
||||
quint64 ConvertSPCStringToNum(const QByteArray &arr);
|
||||
} // namespace SPC
|
||||
|
||||
namespace VGM {
|
||||
// VGM SPEC:
|
||||
// http://www.smspower.org/uploads/Music/vgmspec170.txt?sid=17c810c54633b6dd4982f92f718361c1
|
||||
// GD3 TAG SPEC:
|
||||
// http://www.smspower.org/uploads/Music/gd3spec100.txt
|
||||
constexpr int GD3_TAG_PTR = 0x14;
|
||||
constexpr int SAMPLE_COUNT = 0x18;
|
||||
constexpr int LOOP_SAMPLE_COUNT = 0x20;
|
||||
constexpr int SAMPLE_TIMEBASE = 44100;
|
||||
constexpr int GST_GME_LOOP_TIME_MS = 8000;
|
||||
|
||||
void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
// Takes in two QByteArrays, expected to be 4 bytes long. Desired length is returned via output parameter out_length. Returns false on error.
|
||||
bool GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length);
|
||||
|
||||
} // namespace VGM
|
||||
|
||||
} // namespace GME
|
||||
|
||||
// TagReaderGME
|
||||
// Fulfills Strawberry's Intended interface for tag reading.
|
||||
class TagReaderGME : public TagReaderBase {
|
||||
|
||||
public:
|
||||
explicit TagReaderGME();
|
||||
~TagReaderGME();
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
};
|
||||
|
||||
#endif // TAGREADERGME_H
|
||||
@@ -27,6 +27,8 @@ message SongMetadata {
|
||||
S3M = 19;
|
||||
XM = 20;
|
||||
IT = 21;
|
||||
SPC = 22;
|
||||
VGM = 23;
|
||||
CDDA = 90;
|
||||
STREAM = 91;
|
||||
}
|
||||
@@ -71,8 +73,30 @@ message SongMetadata {
|
||||
|
||||
optional float rating = 32;
|
||||
|
||||
optional bool suspicious_tags = 40;
|
||||
optional string acoustid_id = 33;
|
||||
optional string acoustid_fingerprint = 34;
|
||||
|
||||
optional string musicbrainz_album_artist_id = 35; // MusicBrainz Release Artist ID (MUSICBRAINZ_ALBUMARTISTID)
|
||||
optional string musicbrainz_artist_id = 36; // MusicBrainz Artist ID (MUSICBRAINZ_ARTISTID)
|
||||
optional string musicbrainz_original_artist_id = 37; // MusicBrainz Original Artist ID (MUSICBRAINZ_ORIGINALARTISTID)
|
||||
optional string musicbrainz_album_id = 38; // MusicBrainz Release ID (MUSICBRAINZ_ALBUMID)
|
||||
optional string musicbrainz_original_album_id = 39; // MusicBrainz Original Release ID (MUSICBRAINZ_ORIGINALALBUMID)
|
||||
optional string musicbrainz_recording_id = 40; // MusicBrainz Recording ID (MUSICBRAINZ_TRACKID)
|
||||
optional string musicbrainz_track_id = 41; // MusicBrainz Track ID (MUSICBRAINZ_RELEASETRACKID)
|
||||
optional string musicbrainz_disc_id = 42; // MusicBrainz Disc ID (MUSICBRAINZ_DISCID)
|
||||
optional string musicbrainz_release_group_id = 43; // MusicBrainz Release Group ID (MUSICBRAINZ_RELEASEGROUPID)
|
||||
optional string musicbrainz_work_id = 44; // MusicBrainz Work ID (MUSICBRAINZ_WORKID)
|
||||
|
||||
optional bool suspicious_tags = 50;
|
||||
|
||||
}
|
||||
|
||||
message IsMediaFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message ReadFileRequest {
|
||||
@@ -85,21 +109,20 @@ message ReadFileResponse {
|
||||
|
||||
message SaveFileRequest {
|
||||
optional string filename = 1;
|
||||
optional SongMetadata metadata = 2;
|
||||
optional bool save_tags = 2;
|
||||
optional bool save_playcount = 3;
|
||||
optional bool save_rating = 4;
|
||||
optional bool save_cover = 5;
|
||||
optional SongMetadata metadata = 6;
|
||||
optional string cover_filename = 7;
|
||||
optional bytes cover_data = 8;
|
||||
optional bool cover_is_jpeg = 9;
|
||||
}
|
||||
|
||||
message SaveFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
@@ -110,7 +133,9 @@ message LoadEmbeddedArtResponse {
|
||||
|
||||
message SaveEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
optional bytes data = 2;
|
||||
optional string cover_filename = 2;
|
||||
optional bytes cover_data = 3;
|
||||
optional bool cover_is_jpeg = 4;
|
||||
}
|
||||
|
||||
message SaveEmbeddedArtResponse {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,12 @@
|
||||
#include <taglib/tstring.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/xiphcomment.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/popularimeterframe.h>
|
||||
|
||||
@@ -50,25 +54,24 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
|
||||
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
static void TStringToStdString(const TagLib::String &tag, std::string *output);
|
||||
|
||||
private:
|
||||
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
|
||||
|
||||
static void Decode(const TagLib::String &tag, std::string *output);
|
||||
static void Decode(const QString &tag, std::string *output);
|
||||
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
|
||||
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const;
|
||||
void SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
|
||||
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
@@ -81,6 +84,23 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
|
||||
static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag);
|
||||
|
||||
void SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
|
||||
void SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
|
||||
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const;
|
||||
void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const;
|
||||
void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const;
|
||||
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const;
|
||||
|
||||
private:
|
||||
FileRefFactory *factory_;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
|
||||
TagReaderTagParser::TagReaderTagParser() = default;
|
||||
|
||||
@@ -79,7 +80,7 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
|
||||
}
|
||||
|
||||
const auto tracks = taginfo.tracks();
|
||||
for (const auto track : tracks) {
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
if (track->mediaType() == TagParser::MediaType::Audio) {
|
||||
taginfo.close();
|
||||
return true;
|
||||
@@ -93,17 +94,18 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
qLog(Debug) << "Reading tags from" << filename;
|
||||
|
||||
const QFileInfo fileinfo(filename);
|
||||
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return;
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false;
|
||||
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
const QByteArray basefilename = fileinfo.fileName().toUtf8();
|
||||
|
||||
song->set_basefilename(DataCommaSizeFromQString(fileinfo.fileName()));
|
||||
song->set_basefilename(basefilename.constData(), basefilename.size());
|
||||
song->set_url(url.constData(), url.size());
|
||||
song->set_filesize(fileinfo.size());
|
||||
song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
||||
@@ -135,27 +137,27 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
const auto tracks = taginfo.tracks();
|
||||
for (const auto track : tracks) {
|
||||
std::vector<TagParser::AbstractTrack*> tracks = taginfo.tracks();
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
switch (track->format().general) {
|
||||
case TagParser::GeneralMediaFormat::Flac:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC);
|
||||
@@ -206,10 +208,10 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
|
||||
if (song->filetype() == spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_UNKNOWN) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8));
|
||||
@@ -247,16 +249,43 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
|
||||
taginfo.close();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
catch(...) {
|
||||
return false;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
|
||||
bool TagReaderTagParser::SaveFile(const spb::tagreader::SaveFileRequest &request) const {
|
||||
|
||||
if (filename.isEmpty()) return false;
|
||||
if (request.filename().empty()) return false;
|
||||
|
||||
qLog(Debug) << "Saving tags to" << filename;
|
||||
const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size());
|
||||
const spb::tagreader::SongMetadata song = request.metadata();
|
||||
const bool save_tags = request.has_save_tags() && request.save_tags();
|
||||
const bool save_playcount = request.has_save_playcount() && request.save_playcount();
|
||||
const bool save_rating = request.has_save_rating() && request.save_rating();
|
||||
const bool save_cover = request.has_save_cover() && request.save_cover();
|
||||
|
||||
QStringList save_tags_options;
|
||||
if (save_tags) {
|
||||
save_tags_options << "tags";
|
||||
}
|
||||
if (save_playcount) {
|
||||
save_tags_options << "playcount";
|
||||
}
|
||||
if (save_rating) {
|
||||
save_tags_options << "rating";
|
||||
}
|
||||
if (save_cover) {
|
||||
save_tags_options << "embedded cover";
|
||||
}
|
||||
|
||||
qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename;
|
||||
|
||||
const QByteArray cover_data = LoadCoverDataFromRequest(request);
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
@@ -291,22 +320,34 @@ bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader:
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
|
||||
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
|
||||
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
|
||||
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (save_tags) {
|
||||
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
|
||||
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
|
||||
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
|
||||
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
|
||||
}
|
||||
if (save_playcount) {
|
||||
SaveSongPlaycountToFile(tag, song);
|
||||
}
|
||||
if (save_rating) {
|
||||
SaveSongRatingToFile(tag, song);
|
||||
}
|
||||
if (save_cover) {
|
||||
SaveEmbeddedArt(tag, cover_data);
|
||||
}
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
@@ -354,7 +395,7 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
|
||||
QByteArray data(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize());
|
||||
taginfo.close();
|
||||
@@ -375,12 +416,22 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
|
||||
void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const {
|
||||
|
||||
if (filename.isEmpty()) return false;
|
||||
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderTagParser::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const {
|
||||
|
||||
if (request.filename().empty()) return false;
|
||||
|
||||
const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size());
|
||||
|
||||
qLog(Debug) << "Saving art to" << filename;
|
||||
|
||||
const QByteArray cover_data = LoadCoverDataFromRequest(request);
|
||||
|
||||
try {
|
||||
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
@@ -411,8 +462,8 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveEmbeddedArt(tag, cover_data);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
@@ -431,8 +482,16 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag*, const spb::tagreader::SongMetadata&) const {}
|
||||
|
||||
bool TagReaderTagParser::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const { return false; }
|
||||
|
||||
void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const {
|
||||
|
||||
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
|
||||
|
||||
if (filename.isEmpty()) return false;
|
||||
@@ -472,9 +531,10 @@ bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveSongRatingToFile(tag, song);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <tagparser/tag.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
@@ -39,15 +41,21 @@ class TagReaderTagParser : public TagReaderBase {
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
|
||||
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
private:
|
||||
void SaveSongPlaycountToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const;
|
||||
|
||||
public:
|
||||
Q_DISABLE_COPY(TagReaderTagParser)
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ int main(int argc, char **argv) {
|
||||
qLog(Error) << "First line" << first_line << "does not match" << filepath;
|
||||
success = false;
|
||||
}
|
||||
QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak)?\\)$"));
|
||||
QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak|, reexport)?\\)$"));
|
||||
for (const QString &output_line : output_lines) {
|
||||
|
||||
//qDebug() << "Final check on" << filepath << output_line;
|
||||
@@ -130,9 +130,6 @@ int main(int argc, char **argv) {
|
||||
else if (library.startsWith("/System/Library/") || library.startsWith("/usr/lib/")) { // System library
|
||||
continue;
|
||||
}
|
||||
else if (library.endsWith("libgcc_s.1.dylib")) { // fftw points to it for some reason.
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "File" << filepath << "points to" << library;
|
||||
success = false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
|
||||
@@ -34,28 +34,11 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
|
||||
spb::tagreader::Message reply;
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
reply.mutable_is_media_file_response()->set_success(tag_reader_.IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())));
|
||||
}
|
||||
else if (message.has_read_file_request()) {
|
||||
tag_reader_.ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
|
||||
}
|
||||
else if (message.has_save_file_request()) {
|
||||
reply.mutable_save_file_response()->set_success(tag_reader_.SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata()));
|
||||
}
|
||||
else if (message.has_load_embedded_art_request()) {
|
||||
QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
|
||||
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
|
||||
}
|
||||
else if (message.has_save_embedded_art_request()) {
|
||||
reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast<qint64>(message.save_embedded_art_request().data().size()))));
|
||||
}
|
||||
|
||||
else if (message.has_save_song_playcount_to_file_request()) {
|
||||
reply.mutable_save_song_playcount_to_file_response()->set_success(tag_reader_.SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata()));
|
||||
}
|
||||
else if (message.has_save_song_rating_to_file_request()) {
|
||||
reply.mutable_save_song_rating_to_file_response()->set_success(tag_reader_.SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata()));
|
||||
bool success = HandleMessage(message, reply, &tag_reader_);
|
||||
if (!success) {
|
||||
#if defined(USE_TAGLIB)
|
||||
HandleMessage(message, reply, &tag_reader_gme_);
|
||||
#endif
|
||||
}
|
||||
|
||||
SendReply(message, &reply);
|
||||
@@ -63,7 +46,55 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
}
|
||||
|
||||
void TagReaderWorker::DeviceClosed() {
|
||||
|
||||
AbstractMessageHandler<spb::tagreader::Message>::DeviceClosed();
|
||||
|
||||
QCoreApplication::exit();
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) {
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
const QString filename = QString::fromUtf8(message.is_media_file_request().filename().data(), message.is_media_file_request().filename().size());
|
||||
bool success = reader->IsMediaFile(filename);
|
||||
reply.mutable_is_media_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_read_file_request()) {
|
||||
const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), message.read_file_request().filename().size());
|
||||
bool success = reader->ReadFile(filename, reply.mutable_read_file_response()->mutable_metadata());
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_file_request()) {
|
||||
bool success = reader->SaveFile(message.save_file_request());
|
||||
reply.mutable_save_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_load_embedded_art_request()) {
|
||||
const QString filename = QString::fromUtf8(message.load_embedded_art_request().filename().data(), message.load_embedded_art_request().filename().size());
|
||||
QByteArray data = reader->LoadEmbeddedArt(filename);
|
||||
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
|
||||
return true;
|
||||
}
|
||||
else if (message.has_save_embedded_art_request()) {
|
||||
bool success = reader->SaveEmbeddedArt(message.save_embedded_art_request());
|
||||
reply.mutable_save_embedded_art_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_song_playcount_to_file_request()) {
|
||||
const QString filename = QString::fromUtf8(message.save_song_playcount_to_file_request().filename().data(), message.save_song_playcount_to_file_request().filename().size());
|
||||
bool success = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().metadata());
|
||||
reply.mutable_save_song_playcount_to_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_song_rating_to_file_request()) {
|
||||
const QString filename = QString::fromUtf8(message.save_song_rating_to_file_request().filename().data(), message.save_song_rating_to_file_request().filename().size());
|
||||
bool success = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().metadata());
|
||||
reply.mutable_save_song_rating_to_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
#include "core/messagehandler.h"
|
||||
#if defined(USE_TAGLIB)
|
||||
# include "tagreadertaglib.h"
|
||||
# include "tagreadergme.h"
|
||||
#elif defined(USE_TAGPARSER)
|
||||
# include "tagreadertagparser.h"
|
||||
#endif
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
class QIODevice;
|
||||
@@ -44,8 +46,12 @@ class TagReaderWorker : public AbstractMessageHandler<spb::tagreader::Message> {
|
||||
void DeviceClosed() override;
|
||||
|
||||
private:
|
||||
// Handle message using specific TagReaderBase implementation. Returns true on successful message handle.
|
||||
bool HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase* reader);
|
||||
|
||||
#if defined(USE_TAGLIB)
|
||||
TagReaderTagLib tag_reader_;
|
||||
TagReaderGME tag_reader_gme_;
|
||||
#elif defined(USE_TAGPARSER)
|
||||
TagReaderTagParser tag_reader_;
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
include(../cmake/Translations.cmake)
|
||||
@@ -7,7 +7,6 @@ endif()
|
||||
set(SOURCES
|
||||
core/mainwindow.cpp
|
||||
core/application.cpp
|
||||
core/appearance.cpp
|
||||
core/player.cpp
|
||||
core/commandlineoptions.cpp
|
||||
core/database.cpp
|
||||
@@ -35,14 +34,29 @@ set(SOURCES
|
||||
core/taskmanager.cpp
|
||||
core/thread.cpp
|
||||
core/urlhandler.cpp
|
||||
core/utilities.cpp
|
||||
core/imageutils.cpp
|
||||
core/iconloader.cpp
|
||||
core/standarditemiconloader.cpp
|
||||
core/scopedtransaction.cpp
|
||||
core/translations.cpp
|
||||
core/systemtrayicon.cpp
|
||||
|
||||
utilities/strutils.cpp
|
||||
utilities/envutils.cpp
|
||||
utilities/colorutils.cpp
|
||||
utilities/cryptutils.cpp
|
||||
utilities/fileutils.cpp
|
||||
utilities/diskutils.cpp
|
||||
utilities/imageutils.cpp
|
||||
utilities/macaddrutils.cpp
|
||||
utilities/mimeutils.cpp
|
||||
utilities/randutils.cpp
|
||||
utilities/threadutils.cpp
|
||||
utilities/timeutils.cpp
|
||||
utilities/transliterate.cpp
|
||||
utilities/xmlutils.cpp
|
||||
utilities/filemanagerutils.cpp
|
||||
utilities/coverutils.cpp
|
||||
|
||||
engine/enginetype.cpp
|
||||
engine/enginebase.cpp
|
||||
engine/devicefinders.cpp
|
||||
@@ -54,6 +68,7 @@ set(SOURCES
|
||||
analyzer/blockanalyzer.cpp
|
||||
analyzer/boomanalyzer.cpp
|
||||
analyzer/rainbowanalyzer.cpp
|
||||
analyzer/sonogram.cpp
|
||||
|
||||
equalizer/equalizer.cpp
|
||||
equalizer/equalizerslider.cpp
|
||||
@@ -69,9 +84,11 @@ set(SOURCES
|
||||
collection/collectionitemdelegate.cpp
|
||||
collection/collectionviewcontainer.cpp
|
||||
collection/collectiondirectorymodel.cpp
|
||||
collection/collectionfilteroptions.cpp
|
||||
collection/collectionfilterwidget.cpp
|
||||
collection/collectionplaylistitem.cpp
|
||||
collection/collectionquery.cpp
|
||||
collection/collectionqueryoptions.cpp
|
||||
collection/savedgroupingmanager.cpp
|
||||
collection/groupbydialog.cpp
|
||||
collection/collectiontask.cpp
|
||||
@@ -152,6 +169,8 @@ set(SOURCES
|
||||
|
||||
lyrics/lyricsproviders.cpp
|
||||
lyrics/lyricsprovider.cpp
|
||||
lyrics/lyricssearchrequest.h
|
||||
lyrics/lyricssearchresult.h
|
||||
lyrics/lyricsfetcher.cpp
|
||||
lyrics/lyricsfetchersearch.cpp
|
||||
lyrics/jsonlyricsprovider.cpp
|
||||
@@ -161,6 +180,9 @@ set(SOURCES
|
||||
lyrics/geniuslyricsprovider.cpp
|
||||
lyrics/musixmatchlyricsprovider.cpp
|
||||
lyrics/chartlyricsprovider.cpp
|
||||
lyrics/lyricscomlyricsprovider.cpp
|
||||
|
||||
providers/musixmatchprovider.cpp
|
||||
|
||||
settings/settingsdialog.cpp
|
||||
settings/settingspage.cpp
|
||||
@@ -203,6 +225,8 @@ set(SOURCES
|
||||
widgets/multiloadingindicator.cpp
|
||||
widgets/playingwidget.cpp
|
||||
widgets/renametablineedit.cpp
|
||||
widgets/sliderslider.cpp
|
||||
widgets/prettyslider.cpp
|
||||
widgets/volumeslider.cpp
|
||||
widgets/stickyslider.cpp
|
||||
widgets/stretchheaderview.cpp
|
||||
@@ -246,6 +270,7 @@ set(SOURCES
|
||||
scrobbler/scrobblerservice.cpp
|
||||
scrobbler/scrobblercache.cpp
|
||||
scrobbler/scrobblercacheitem.cpp
|
||||
scrobbler/scrobblemetadata.cpp
|
||||
scrobbler/scrobblingapi20.cpp
|
||||
scrobbler/lastfmscrobbler.cpp
|
||||
scrobbler/librefmscrobbler.cpp
|
||||
@@ -262,7 +287,6 @@ set(SOURCES
|
||||
set(HEADERS
|
||||
core/mainwindow.h
|
||||
core/application.h
|
||||
core/appearance.h
|
||||
core/player.h
|
||||
core/database.h
|
||||
core/deletefiles.h
|
||||
@@ -292,6 +316,7 @@ set(HEADERS
|
||||
analyzer/blockanalyzer.h
|
||||
analyzer/boomanalyzer.h
|
||||
analyzer/rainbowanalyzer.h
|
||||
analyzer/sonogram.h
|
||||
|
||||
equalizer/equalizer.h
|
||||
equalizer/equalizerslider.h
|
||||
@@ -393,6 +418,7 @@ set(HEADERS
|
||||
lyrics/geniuslyricsprovider.h
|
||||
lyrics/musixmatchlyricsprovider.h
|
||||
lyrics/chartlyricsprovider.h
|
||||
lyrics/lyricscomlyricsprovider.h
|
||||
|
||||
settings/settingsdialog.h
|
||||
settings/settingspage.h
|
||||
@@ -434,6 +460,8 @@ set(HEADERS
|
||||
widgets/multiloadingindicator.h
|
||||
widgets/playingwidget.h
|
||||
widgets/renametablineedit.h
|
||||
widgets/sliderslider.h
|
||||
widgets/prettyslider.h
|
||||
widgets/volumeslider.h
|
||||
widgets/stickyslider.h
|
||||
widgets/stretchheaderview.h
|
||||
@@ -444,7 +472,6 @@ set(HEADERS
|
||||
widgets/qsearchfield.h
|
||||
widgets/ratingwidget.h
|
||||
widgets/forcescrollperpixel.h
|
||||
widgets/resizabletextedit.h
|
||||
|
||||
osd/osdbase.h
|
||||
osd/osdpretty.h
|
||||
@@ -476,7 +503,6 @@ set(HEADERS
|
||||
scrobbler/scrobblerservices.h
|
||||
scrobbler/scrobblerservice.h
|
||||
scrobbler/scrobblercache.h
|
||||
scrobbler/scrobblercacheitem.h
|
||||
scrobbler/scrobblingapi20.h
|
||||
scrobbler/lastfmscrobbler.h
|
||||
scrobbler/librefmscrobbler.h
|
||||
@@ -570,7 +596,7 @@ option(USE_INSTALL_PREFIX "Look for data in CMAKE_INSTALL_PREFIX" ON)
|
||||
|
||||
if(NOT APPLE)
|
||||
set(NOT_APPLE ON)
|
||||
optional_source(NOT_APPLE SOURCES widgets/qsearchfield_nonmac.cpp core/qtsystemtrayicon.cpp HEADERS core/qtsystemtrayicon.h)
|
||||
optional_source(NOT_APPLE SOURCES widgets/qsearchfield_qt.cpp core/qtsystemtrayicon.cpp HEADERS core/qtsystemtrayicon.h)
|
||||
endif()
|
||||
|
||||
if(HAVE_GLOBALSHORTCUTS)
|
||||
@@ -610,43 +636,42 @@ optional_source(HAVE_VLC SOURCES engine/vlcengine.cpp HEADERS engine/vlcengine.h
|
||||
|
||||
# DBUS and MPRIS - Unix specific
|
||||
if(UNIX AND HAVE_DBUS)
|
||||
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dbus)
|
||||
|
||||
optional_source(HAVE_DBUS SOURCES core/mpris2.cpp HEADERS core/mpris2.h)
|
||||
optional_source(HAVE_UDISKS2 SOURCES device/udisks2lister.cpp HEADERS device/udisks2lister.h)
|
||||
|
||||
# MPRIS 2.0 DBUS interfaces
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.Player.xml core/mpris2.h mpris::Mpris2 core/mpris2_player Mpris2Player)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.xml core/mpris2.h mpris::Mpris2 core/mpris2_root Mpris2Root)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.TrackList.xml core/mpris2.h mpris::Mpris2 core/mpris2_tracklist Mpris2TrackList)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.Player.xml core/mpris2.h mpris::Mpris2 mpris2_player Mpris2Player)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.xml core/mpris2.h mpris::Mpris2 mpris2_root Mpris2Root)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.TrackList.xml core/mpris2.h mpris::Mpris2 mpris2_tracklist Mpris2TrackList)
|
||||
|
||||
# MPRIS 2.1 DBUS interfaces
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.Playlists.xml core/mpris2.h mpris::Mpris2 core/mpris2_playlists Mpris2Playlists)
|
||||
qt_add_dbus_adaptor(SOURCES dbus/org.mpris.MediaPlayer2.Playlists.xml core/mpris2.h mpris::Mpris2 mpris2_playlists Mpris2Playlists)
|
||||
|
||||
# org.freedesktop.Notifications DBUS interface
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.Notifications.xml dbus/notification)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.Notifications.xml notification)
|
||||
|
||||
# org.gnome.SettingsDaemon interface
|
||||
qt_add_dbus_interface(SOURCES dbus/org.gnome.SettingsDaemon.MediaKeys.xml dbus/gnomesettingsdaemon)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.gnome.SettingsDaemon.MediaKeys.xml gnomesettingsdaemon)
|
||||
|
||||
# org.mate.SettingsDaemon interface
|
||||
qt_add_dbus_interface(SOURCES dbus/org.mate.SettingsDaemon.MediaKeys.xml dbus/matesettingsdaemon)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.mate.SettingsDaemon.MediaKeys.xml matesettingsdaemon)
|
||||
|
||||
# org.kde.KGlobalAccel interface
|
||||
qt_add_dbus_interface(SOURCES dbus/org.kde.KGlobalAccel.xml dbus/kglobalaccel)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.kde.KGlobalAccel.Component.xml dbus/kglobalaccelcomponent)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.kde.KGlobalAccel.xml kglobalaccel)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.kde.KGlobalAccel.Component.xml kglobalaccelcomponent)
|
||||
|
||||
if(HAVE_UDISKS2)
|
||||
set_source_files_properties(dbus/org.freedesktop.DBus.ObjectManager.xml PROPERTIES NO_NAMESPACE dbus/objectmanager INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Filesystem.xml PROPERTIES NO_NAMESPACE dbus/udisks2filesystem INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Block.xml PROPERTIES NO_NAMESPACE dbus/udisks2block INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Drive.xml PROPERTIES NO_NAMESPACE dbus/udisks2drive INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Job.xml PROPERTIES NO_NAMESPACE dbus/udisks2job INCLUDE dbus/metatypes.h)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.DBus.ObjectManager.xml dbus/objectmanager)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Filesystem.xml dbus/udisks2filesystem)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Block.xml dbus/udisks2block)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Drive.xml dbus/udisks2drive)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Job.xml dbus/udisks2job)
|
||||
set_source_files_properties(dbus/org.freedesktop.DBus.ObjectManager.xml PROPERTIES NO_NAMESPACE objectmanager INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Filesystem.xml PROPERTIES NO_NAMESPACE udisks2filesystem INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Block.xml PROPERTIES NO_NAMESPACE udisks2block INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Drive.xml PROPERTIES NO_NAMESPACE udisks2drive INCLUDE dbus/metatypes.h)
|
||||
set_source_files_properties(dbus/org.freedesktop.UDisks2.Job.xml PROPERTIES NO_NAMESPACE udisks2job INCLUDE dbus/metatypes.h)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.DBus.ObjectManager.xml objectmanager)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Filesystem.xml udisks2filesystem)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Block.xml udisks2block)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Drive.xml udisks2drive)
|
||||
qt_add_dbus_interface(SOURCES dbus/org.freedesktop.UDisks2.Job.xml udisks2job)
|
||||
endif(HAVE_UDISKS2)
|
||||
|
||||
endif(UNIX AND HAVE_DBUS)
|
||||
@@ -782,8 +807,8 @@ optional_source(HAVE_AUDIOCD
|
||||
# Platform specific - macOS
|
||||
optional_source(APPLE
|
||||
SOURCES
|
||||
utilities/macosutils.mm
|
||||
core/scoped_nsautorelease_pool.mm
|
||||
core/mac_utilities.mm
|
||||
core/mac_startup.mm
|
||||
core/macsystemtrayicon.mm
|
||||
core/macfslistener.mm
|
||||
@@ -804,8 +829,10 @@ optional_source(APPLE
|
||||
# Platform specific - Windows
|
||||
optional_source(WIN32
|
||||
SOURCES
|
||||
utilities/winutils.cpp
|
||||
engine/directsounddevicefinder.cpp
|
||||
engine/mmdevicefinder.cpp
|
||||
core/scopedwchararray.cpp
|
||||
core/windows7thumbbar.cpp
|
||||
HEADERS
|
||||
core/windows7thumbbar.h
|
||||
@@ -942,11 +969,17 @@ link_directories(
|
||||
${GOBJECT_LIBRARY_DIRS}
|
||||
${GNUTLS_LIBRARY_DIRS}
|
||||
${SQLITE_LIBRARY_DIRS}
|
||||
${PROTOBUF_LIBRARY_DIRS}
|
||||
${SINGLEAPPLICATION_LIBRARY_DIRS}
|
||||
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
|
||||
${Iconv_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
if(HAVE_ICU)
|
||||
link_directories(${ICU_LIBRARY_DIRS})
|
||||
else()
|
||||
link_directories(${Iconv_LIBRARY_DIRS})
|
||||
endif()
|
||||
|
||||
if(HAVE_ALSA)
|
||||
link_directories(${ALSA_LIBRARY_DIRS})
|
||||
endif()
|
||||
@@ -1056,13 +1089,24 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
${GNUTLS_LIBRARIES}
|
||||
${SQLITE_LIBRARIES}
|
||||
${QT_LIBRARIES}
|
||||
${Protobuf_LIBRARIES}
|
||||
${SINGLEAPPLICATION_LIBRARIES}
|
||||
${SINGLECOREAPPLICATION_LIBRARIES}
|
||||
${Iconv_LIBRARIES}
|
||||
libstrawberry-common
|
||||
libstrawberry-tagreader
|
||||
)
|
||||
|
||||
if(HAVE_ICU)
|
||||
target_include_directories(strawberry_lib SYSTEM PRIVATE ${ICU_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry_lib PRIVATE ${ICU_LIBRARIES})
|
||||
else()
|
||||
if(FREEBSD AND NOT Iconv_LIBRARIES)
|
||||
set(Iconv_LIBRARIES iconv)
|
||||
endif()
|
||||
target_include_directories(strawberry_lib SYSTEM PRIVATE ${Iconv_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry_lib PRIVATE ${Iconv_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_ALSA)
|
||||
target_include_directories(strawberry_lib SYSTEM PRIVATE ${ALSA_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry_lib PRIVATE ${ALSA_LIBRARIES})
|
||||
@@ -1141,10 +1185,6 @@ if(HAVE_LIBMTP)
|
||||
target_link_libraries(strawberry_lib PRIVATE ${LIBMTP_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(FREEBSD)
|
||||
target_link_libraries(strawberry_lib PRIVATE iconv)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(strawberry_lib PRIVATE
|
||||
"-framework AppKit"
|
||||
|
||||
@@ -57,7 +57,11 @@ Analyzer::Base::Base(QWidget *parent, const uint scopeSize)
|
||||
lastscope_(512),
|
||||
new_frame_(false),
|
||||
is_playing_(false),
|
||||
timeout_(40) {}
|
||||
timeout_(40) {
|
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
||||
|
||||
}
|
||||
|
||||
Analyzer::Base::~Base() {
|
||||
delete fht_;
|
||||
@@ -104,7 +108,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
|
||||
p.fillRect(e->rect(), palette().color(QPalette::Window));
|
||||
|
||||
switch (engine_->state()) {
|
||||
case Engine::Playing: {
|
||||
case Engine::State::Playing: {
|
||||
const Engine::Scope &thescope = engine_->scope(timeout_);
|
||||
int i = 0;
|
||||
|
||||
@@ -122,7 +126,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
|
||||
|
||||
break;
|
||||
}
|
||||
case Engine::Paused:
|
||||
case Engine::State::Paused:
|
||||
is_playing_ = false;
|
||||
analyze(p, lastscope_, new_frame_);
|
||||
break;
|
||||
|
||||
@@ -49,7 +49,7 @@ class QTimerEvent;
|
||||
|
||||
namespace Analyzer {
|
||||
|
||||
typedef std::vector<float> Scope;
|
||||
using Scope = std::vector<float>;
|
||||
|
||||
class Base : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include <QActionGroup>
|
||||
#include <QSettings>
|
||||
#include <QtEvents>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "analyzercontainer.h"
|
||||
|
||||
@@ -41,6 +40,7 @@
|
||||
#include "blockanalyzer.h"
|
||||
#include "boomanalyzer.h"
|
||||
#include "rainbowanalyzer.h"
|
||||
#include "sonogram.h"
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "engine/enginebase.h"
|
||||
@@ -86,6 +86,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
AddAnalyzerType<BoomAnalyzer>();
|
||||
AddAnalyzerType<Rainbow::NyanCatAnalyzer>();
|
||||
AddAnalyzerType<Rainbow::RainbowDashAnalyzer>();
|
||||
AddAnalyzerType<Sonogram>();
|
||||
|
||||
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
|
||||
disable_action_->setCheckable(true);
|
||||
|
||||
@@ -110,7 +110,7 @@ void BoomAnalyzer::transform(Scope &s) {
|
||||
|
||||
void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame) {
|
||||
|
||||
if (!new_frame || engine_->state() == Engine::Paused) {
|
||||
if (!new_frame || engine_->state() == Engine::State::Paused) {
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
return;
|
||||
}
|
||||
|
||||
92
src/analyzer/sonogram.cpp
Normal file
92
src/analyzer/sonogram.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Strawberry Music Player
|
||||
This file was part of Clementine.
|
||||
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
|
||||
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
|
||||
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
|
||||
#include "sonogram.h"
|
||||
|
||||
const char *Sonogram::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram");
|
||||
|
||||
Sonogram::Sonogram(QWidget *parent)
|
||||
: Analyzer::Base(parent, 9) {}
|
||||
|
||||
void Sonogram::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
Q_UNUSED(e)
|
||||
|
||||
canvas_ = QPixmap(size());
|
||||
canvas_.fill(palette().color(QPalette::Window));
|
||||
|
||||
}
|
||||
|
||||
void Sonogram::analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) {
|
||||
|
||||
if (!new_frame || engine_->state() == Engine::State::Paused) {
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter canvas_painter(&canvas_);
|
||||
canvas_painter.drawPixmap(0, 0, canvas_, 1, 0, width() - 1, -1);
|
||||
|
||||
Analyzer::Scope::const_iterator it = s.begin(), end = s.end();
|
||||
|
||||
for (int y = height() - 1; y;) {
|
||||
QColor c;
|
||||
if (it >= end || *it < .005) {
|
||||
c = palette().color(QPalette::Window);
|
||||
}
|
||||
else if (*it < .05) {
|
||||
c.setHsv(95, 255, 255 - static_cast<int>(*it * 4000.0));
|
||||
}
|
||||
else if (*it < 1.0) {
|
||||
c.setHsv(95 - static_cast<int>(*it * 90.0), 255, 255);
|
||||
}
|
||||
else {
|
||||
c = Qt::red;
|
||||
}
|
||||
|
||||
canvas_painter.setPen(c);
|
||||
canvas_painter.drawPoint(width() - 1, y--);
|
||||
|
||||
if (it < end) ++it;
|
||||
}
|
||||
|
||||
canvas_painter.end();
|
||||
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
|
||||
}
|
||||
|
||||
void Sonogram::transform(Analyzer::Scope &scope) {
|
||||
|
||||
fht_->power2(scope.data());
|
||||
fht_->scale(scope.data(), 1.0 / 256);
|
||||
scope.resize(fht_->size() / 2);
|
||||
|
||||
}
|
||||
|
||||
void Sonogram::demo(QPainter &p) {
|
||||
analyze(p, Analyzer::Scope(fht_->size(), 0), new_frame_);
|
||||
}
|
||||
49
src/analyzer/sonogram.h
Normal file
49
src/analyzer/sonogram.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Strawberry Music Player
|
||||
This file was part of Clementine.
|
||||
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
|
||||
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2015, Mark Furneaux <mark@furneaux.ca>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SONOGRAM_H
|
||||
#define SONOGRAM_H
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "analyzerbase.h"
|
||||
|
||||
class Sonogram : public Analyzer::Base {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE explicit Sonogram(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) override;
|
||||
void transform(Analyzer::Scope &scope) override;
|
||||
void demo(QPainter &p) override;
|
||||
|
||||
private:
|
||||
QPixmap canvas_;
|
||||
};
|
||||
|
||||
#endif // SONOGRAM_H
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <QList>
|
||||
#include <QSettings>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/taskmanager.h"
|
||||
@@ -35,9 +34,9 @@
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/thread.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "utilities/threadutils.h"
|
||||
#include "collection.h"
|
||||
#include "collectionwatcher.h"
|
||||
#include "collectionbackend.h"
|
||||
@@ -59,8 +58,6 @@ SCollection::SCollection(Application *app, QObject *parent)
|
||||
watcher_(nullptr),
|
||||
watcher_thread_(nullptr),
|
||||
original_thread_(nullptr),
|
||||
io_priority_(Utilities::IoPriority::IOPRIO_CLASS_IDLE),
|
||||
thread_priority_(QThread::Priority::IdlePriority),
|
||||
save_playcounts_to_files_(false),
|
||||
save_ratings_to_files_(false) {
|
||||
|
||||
@@ -70,7 +67,7 @@ SCollection::SCollection(Application *app, QObject *parent)
|
||||
backend()->moveToThread(app->database()->thread());
|
||||
qLog(Debug) << backend_ << "moved to thread" << app->database()->thread();
|
||||
|
||||
backend_->Init(app->database(), app->task_manager(), Song::Source_Collection, kSongsTable, kFtsTable, kDirsTable, kSubdirsTable);
|
||||
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, kSongsTable, kFtsTable, kDirsTable, kSubdirsTable);
|
||||
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
|
||||
@@ -94,20 +91,16 @@ SCollection::~SCollection() {
|
||||
|
||||
void SCollection::Init() {
|
||||
|
||||
watcher_ = new CollectionWatcher(Song::Source_Collection);
|
||||
watcher_ = new CollectionWatcher(Song::Source::Collection);
|
||||
watcher_thread_ = new Thread(this);
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
if (io_priority_ != Utilities::IoPriority::IOPRIO_CLASS_NONE) {
|
||||
watcher_thread_->SetIoPriority(io_priority_);
|
||||
}
|
||||
#endif
|
||||
watcher_thread_->SetIoPriority(Utilities::IoPriority::IOPRIO_CLASS_IDLE);
|
||||
|
||||
watcher_->moveToThread(watcher_thread_);
|
||||
|
||||
qLog(Debug) << watcher_ << "moved to thread" << watcher_thread_ << "with I/O priority" << io_priority_ << "and thread priority" << thread_priority_;
|
||||
qLog(Debug) << watcher_ << "moved to thread" << watcher_thread_;
|
||||
|
||||
watcher_thread_->start(thread_priority_);
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
|
||||
watcher_->set_backend(backend_);
|
||||
watcher_->set_task_manager(app_->task_manager());
|
||||
@@ -170,7 +163,9 @@ void SCollection::AbortScan() { watcher_->Stop(); }
|
||||
void SCollection::Rescan(const SongList &songs) {
|
||||
|
||||
qLog(Debug) << "Rescan" << songs.size() << "songs";
|
||||
if (!songs.isEmpty()) watcher_->RescanTracksAsync(songs);
|
||||
if (!songs.isEmpty()) {
|
||||
watcher_->RescanSongsAsync(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -185,8 +180,6 @@ void SCollection::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
io_priority_ = static_cast<Utilities::IoPriority>(s.value("io_priority", Utilities::IOPRIO_CLASS_IDLE).toInt());
|
||||
thread_priority_ = static_cast<QThread::Priority>(s.value("thread_priority", QThread::Priority::IdlePriority).toInt());
|
||||
save_playcounts_to_files_ = s.value("save_playcounts", false).toBool();
|
||||
save_ratings_to_files_ = s.value("save_ratings", false).toBool();
|
||||
s.endGroup();
|
||||
@@ -220,9 +213,9 @@ void SCollection::SyncPlaycountAndRatingToFiles() {
|
||||
|
||||
}
|
||||
|
||||
void SCollection::SongsPlaycountChanged(const SongList &songs) {
|
||||
void SCollection::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_playcounts_to_files_) {
|
||||
if (save_tags || save_playcounts_to_files_) {
|
||||
app_->tag_reader_client()->UpdateSongsPlaycount(songs);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,10 @@
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
class QThread;
|
||||
class Application;
|
||||
class Thread;
|
||||
class CollectionBackend;
|
||||
@@ -78,7 +77,7 @@ class SCollection : public QObject {
|
||||
|
||||
private slots:
|
||||
void ExitReceived();
|
||||
void SongsPlaycountChanged(const SongList &songs);
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||
|
||||
signals:
|
||||
@@ -99,8 +98,6 @@ class SCollection : public QObject {
|
||||
|
||||
QList<QObject*> wait_for_exit_;
|
||||
|
||||
Utilities::IoPriority io_priority_;
|
||||
QThread::Priority thread_priority_;
|
||||
bool save_playcounts_to_files_;
|
||||
bool save_ratings_to_files_;
|
||||
};
|
||||
|
||||
@@ -50,8 +50,9 @@
|
||||
#include "core/sqlrow.h"
|
||||
#include "smartplaylists/smartplaylistsearch.h"
|
||||
|
||||
#include "directory.h"
|
||||
#include "collectiondirectory.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionquery.h"
|
||||
#include "collectiontask.h"
|
||||
|
||||
@@ -59,7 +60,7 @@ CollectionBackend::CollectionBackend(QObject *parent)
|
||||
: CollectionBackendInterface(parent),
|
||||
db_(nullptr),
|
||||
task_manager_(nullptr),
|
||||
source_(Song::Source_Unknown),
|
||||
source_(Song::Source::Unknown),
|
||||
original_thread_(nullptr) {
|
||||
|
||||
original_thread_ = thread();
|
||||
@@ -139,18 +140,22 @@ void CollectionBackend::IncrementSkipCountAsync(const int id, const float progre
|
||||
QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
|
||||
}
|
||||
|
||||
void CollectionBackend::ResetStatisticsAsync(const int id) {
|
||||
QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id));
|
||||
void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) {
|
||||
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags));
|
||||
}
|
||||
|
||||
void CollectionBackend::ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags) {
|
||||
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList<int>, id_list), Q_ARG(bool, save_tags));
|
||||
}
|
||||
|
||||
void CollectionBackend::LoadDirectories() {
|
||||
|
||||
DirectoryList dirs = GetAllDirectories();
|
||||
CollectionDirectoryList dirs = GetAllDirectories();
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
for (const Directory &dir : dirs) {
|
||||
for (const CollectionDirectory &dir : dirs) {
|
||||
emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db));
|
||||
}
|
||||
|
||||
@@ -207,12 +212,12 @@ void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, con
|
||||
|
||||
}
|
||||
|
||||
DirectoryList CollectionBackend::GetAllDirectories() {
|
||||
CollectionDirectoryList CollectionBackend::GetAllDirectories() {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
DirectoryList ret;
|
||||
CollectionDirectoryList ret;
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_));
|
||||
@@ -222,7 +227,7 @@ DirectoryList CollectionBackend::GetAllDirectories() {
|
||||
}
|
||||
|
||||
while (q.next()) {
|
||||
Directory dir;
|
||||
CollectionDirectory dir;
|
||||
dir.id = q.value(0).toInt();
|
||||
dir.path = q.value(1).toString();
|
||||
|
||||
@@ -232,7 +237,7 @@ DirectoryList CollectionBackend::GetAllDirectories() {
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) {
|
||||
CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db = db_->Connect();
|
||||
@@ -240,19 +245,19 @@ SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) {
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) {
|
||||
CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_));
|
||||
q.BindValue(":dir", id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SubdirectoryList();
|
||||
return CollectionSubdirectoryList();
|
||||
}
|
||||
|
||||
SubdirectoryList subdirs;
|
||||
CollectionSubdirectoryList subdirs;
|
||||
while (q.next()) {
|
||||
Subdirectory subdir;
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.directory_id = id;
|
||||
subdir.path = q.value(0).toString();
|
||||
subdir.mtime = q.value(1).toLongLong();
|
||||
@@ -339,15 +344,15 @@ void CollectionBackend::AddDirectory(const QString &path) {
|
||||
return;
|
||||
}
|
||||
|
||||
Directory dir;
|
||||
CollectionDirectory dir;
|
||||
dir.path = canonical_path;
|
||||
dir.id = q.lastInsertId().toInt();
|
||||
|
||||
emit DirectoryDiscovered(dir, SubdirectoryList());
|
||||
emit DirectoryDiscovered(dir, CollectionSubdirectoryList());
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::RemoveDirectory(const Directory &dir) {
|
||||
void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
@@ -447,13 +452,13 @@ void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_f
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) {
|
||||
void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const Subdirectory &subdir : subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (subdir.mtime == 0) {
|
||||
// Delete the subdirectory
|
||||
SqlQuery q(db);
|
||||
@@ -713,7 +718,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
||||
|
||||
Song old_song = old_songs[new_song.song_id()];
|
||||
|
||||
if (!new_song.IsMetadataEqual(old_song)) { // Update existing song.
|
||||
if (!new_song.IsAllMetadataEqual(old_song) || !new_song.IsFingerprintEqual(old_song)) { // Update existing song.
|
||||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
@@ -900,12 +905,12 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u
|
||||
|
||||
}
|
||||
|
||||
QStringList CollectionBackend::GetAll(const QString &column, const QueryOptions &opt) {
|
||||
QStringList CollectionBackend::GetAll(const QString &column, const CollectionFilterOptions &filter_options) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, fts_table_, filter_options);
|
||||
query.SetColumnSpec("DISTINCT " + column);
|
||||
query.AddCompilationRequirement(false);
|
||||
|
||||
@@ -922,12 +927,12 @@ QStringList CollectionBackend::GetAll(const QString &column, const QueryOptions
|
||||
|
||||
}
|
||||
|
||||
QStringList CollectionBackend::GetAllArtists(const QueryOptions &opt) {
|
||||
QStringList CollectionBackend::GetAllArtists(const CollectionFilterOptions &opt) {
|
||||
|
||||
return GetAll("artist", opt);
|
||||
}
|
||||
|
||||
QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt) {
|
||||
QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOptions &opt) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
@@ -967,15 +972,15 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt)
|
||||
|
||||
}
|
||||
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const QueryOptions &opt) {
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const CollectionFilterOptions &opt) {
|
||||
return GetAlbums(QString(), false, opt);
|
||||
}
|
||||
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const QueryOptions &opt) {
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt) {
|
||||
return GetAlbums(artist, false, opt);
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt) {
|
||||
SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt) {
|
||||
|
||||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
@@ -993,7 +998,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist,
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt) {
|
||||
SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt) {
|
||||
|
||||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
@@ -1012,7 +1017,7 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist,
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetSongsByAlbum(const QString &album, const QueryOptions &opt) {
|
||||
SongList CollectionBackend::GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt) {
|
||||
|
||||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
@@ -1285,11 +1290,11 @@ SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) {
|
||||
}
|
||||
|
||||
|
||||
CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const QueryOptions &opt) {
|
||||
CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const CollectionFilterOptions &opt) {
|
||||
return GetAlbums(QString(), true, opt);
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetCompilationSongs(const QString &album, const QueryOptions &opt) {
|
||||
SongList CollectionBackend::GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
@@ -1314,10 +1319,6 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Quer
|
||||
|
||||
}
|
||||
|
||||
Song::Source CollectionBackend::Source() const {
|
||||
return source_;
|
||||
}
|
||||
|
||||
void CollectionBackend::CompilationsNeedUpdating() {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
@@ -1430,7 +1431,7 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &del
|
||||
|
||||
}
|
||||
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt) {
|
||||
CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
@@ -1520,7 +1521,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
|
||||
ret.album = album;
|
||||
ret.album_artist = effective_albumartist;
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, QueryOptions());
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec("art_automatic, art_manual, url");
|
||||
if (!effective_albumartist.isEmpty()) {
|
||||
query.AddWhere("effective_albumartist", effective_albumartist);
|
||||
@@ -1779,23 +1780,48 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::ResetStatistics(const int id) {
|
||||
void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) {
|
||||
|
||||
if (id == -1) return;
|
||||
|
||||
ResetPlayStatistics(QList<int>() << id, save_tags);
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::ResetPlayStatistics(const QList<int> &id_list, const bool save_tags) {
|
||||
|
||||
if (id_list.isEmpty()) return;
|
||||
|
||||
QStringList id_str_list;
|
||||
id_str_list.reserve(id_list.count());
|
||||
for (const int id : id_list) {
|
||||
id_str_list << QString::number(id);
|
||||
}
|
||||
|
||||
const bool success = ResetPlayStatistics(id_str_list);
|
||||
if (success) {
|
||||
const SongList songs = GetSongsById(id_list);
|
||||
emit SongsStatisticsChanged(songs, save_tags);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) {
|
||||
|
||||
if (id_str_list.isEmpty()) return false;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(":id", id);
|
||||
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_));
|
||||
q.BindValue(":ids", id_str_list.join(","));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Song new_song = GetSongById(id, db);
|
||||
emit SongsStatisticsChanged(SongList() << new_song);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -1867,7 +1893,7 @@ SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &s
|
||||
SongList CollectionBackend::SmartPlaylistsGetAllSongs() {
|
||||
|
||||
// Get all the songs!
|
||||
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
|
||||
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::SearchType::All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::SortType::FieldAsc, SmartPlaylistSearchTerm::Field::Artist, -1));
|
||||
|
||||
}
|
||||
|
||||
@@ -1930,7 +1956,7 @@ void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &a
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
|
||||
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags) {
|
||||
|
||||
SongList songs = GetSongsBy(artist, QString(), title);
|
||||
if (songs.isEmpty()) {
|
||||
@@ -1952,7 +1978,7 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
|
||||
}
|
||||
}
|
||||
|
||||
emit SongsStatisticsChanged(SongList() << songs);
|
||||
emit SongsStatisticsChanged(SongList() << songs, save_tags);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/sqlquery.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionquery.h"
|
||||
#include "directory.h"
|
||||
#include "collectiondirectory.h"
|
||||
|
||||
class QThread;
|
||||
class TaskManager;
|
||||
@@ -53,7 +54,7 @@ class CollectionBackendInterface : public QObject {
|
||||
explicit CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
struct Album {
|
||||
Album() {}
|
||||
Album() : filetype(Song::FileType::Unknown) {}
|
||||
Album(const QString &_album_artist, const QString &_album, const QUrl &_art_automatic, const QUrl &_art_manual, const QList<QUrl> &_urls, const Song::FileType _filetype, const QString &_cue_path)
|
||||
: album_artist(_album_artist),
|
||||
album(_album),
|
||||
@@ -72,11 +73,13 @@ class CollectionBackendInterface : public QObject {
|
||||
Song::FileType filetype;
|
||||
QString cue_path;
|
||||
};
|
||||
typedef QList<Album> AlbumList;
|
||||
using AlbumList = QList<Album>;
|
||||
|
||||
virtual QString songs_table() const = 0;
|
||||
virtual QString fts_table() const = 0;
|
||||
|
||||
virtual Song::Source source() const = 0;
|
||||
|
||||
virtual Database *db() const = 0;
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
@@ -88,23 +91,23 @@ class CollectionBackendInterface : public QObject {
|
||||
|
||||
virtual SongList FindSongsInDirectory(const int id) = 0;
|
||||
virtual SongList SongsWithMissingFingerprint(const int id) = 0;
|
||||
virtual SubdirectoryList SubdirsInDirectory(const int id) = 0;
|
||||
virtual DirectoryList GetAllDirectories() = 0;
|
||||
virtual CollectionSubdirectoryList SubdirsInDirectory(const int id) = 0;
|
||||
virtual CollectionDirectoryList GetAllDirectories() = 0;
|
||||
virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0;
|
||||
|
||||
virtual SongList GetAllSongs() = 0;
|
||||
|
||||
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual SongList GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
|
||||
virtual SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
|
||||
virtual AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
virtual AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
|
||||
|
||||
virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) = 0;
|
||||
virtual void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) = 0;
|
||||
@@ -122,7 +125,7 @@ class CollectionBackendInterface : public QObject {
|
||||
virtual Song GetSongByUrl(const QUrl &url, const qint64 beginning = 0) = 0;
|
||||
|
||||
virtual void AddDirectory(const QString &path) = 0;
|
||||
virtual void RemoveDirectory(const Directory &dir) = 0;
|
||||
virtual void RemoveDirectory(const CollectionDirectory &dir) = 0;
|
||||
};
|
||||
|
||||
class CollectionBackend : public CollectionBackendInterface {
|
||||
@@ -139,6 +142,8 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
|
||||
void ReportErrors(const CollectionQuery &query);
|
||||
|
||||
Song::Source source() const override { return source_; }
|
||||
|
||||
Database *db() const override { return db_; }
|
||||
|
||||
QString songs_table() const override { return songs_table_; }
|
||||
@@ -155,24 +160,24 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
|
||||
SongList FindSongsInDirectory(const int id) override;
|
||||
SongList SongsWithMissingFingerprint(const int id) override;
|
||||
SubdirectoryList SubdirsInDirectory(const int id) override;
|
||||
DirectoryList GetAllDirectories() override;
|
||||
CollectionSubdirectoryList SubdirsInDirectory(const int id) override;
|
||||
CollectionDirectoryList GetAllDirectories() override;
|
||||
void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override;
|
||||
|
||||
SongList GetAllSongs() override;
|
||||
|
||||
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions());
|
||||
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override;
|
||||
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override;
|
||||
SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) override;
|
||||
SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) override;
|
||||
SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) override;
|
||||
QStringList GetAll(const QString &column, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
|
||||
QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
SongList GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
|
||||
SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) override;
|
||||
SongList GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
|
||||
AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) override;
|
||||
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) override;
|
||||
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) override;
|
||||
AlbumList GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
|
||||
|
||||
void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) override;
|
||||
void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) override;
|
||||
@@ -188,14 +193,15 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) override;
|
||||
|
||||
void AddDirectory(const QString &path) override;
|
||||
void RemoveDirectory(const Directory &dir) override;
|
||||
void RemoveDirectory(const CollectionDirectory &dir) override;
|
||||
|
||||
bool ExecCollectionQuery(CollectionQuery *query, SongList &songs);
|
||||
bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs);
|
||||
|
||||
void IncrementPlayCountAsync(const int id);
|
||||
void IncrementSkipCountAsync(const int id, const float progress);
|
||||
void ResetStatisticsAsync(const int id);
|
||||
void ResetPlayStatisticsAsync(const int id, const bool save_tags = false);
|
||||
void ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags = false);
|
||||
|
||||
void DeleteAllAsync();
|
||||
|
||||
@@ -207,8 +213,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
SongList SmartPlaylistsGetAllSongs();
|
||||
SongList SmartPlaylistsFindSongs(const SmartPlaylistSearch &search);
|
||||
|
||||
Song::Source Source() const;
|
||||
|
||||
void AddOrUpdateSongsAsync(const SongList &songs);
|
||||
void UpdateSongsBySongIDAsync(const SongMap &new_songs);
|
||||
|
||||
@@ -226,20 +230,22 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
void UpdateMTimesOnly(const SongList &songs);
|
||||
void DeleteSongs(const SongList &songs);
|
||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
|
||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false);
|
||||
void UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false);
|
||||
void ForceCompilation(const QString &album, const QList<QString> &artists, const bool on);
|
||||
void IncrementPlayCount(const int id);
|
||||
void IncrementSkipCount(const int id, const float progress);
|
||||
void ResetStatistics(const int id);
|
||||
void ResetPlayStatistics(const int id, const bool save_tags = false);
|
||||
void ResetPlayStatistics(const QList<int> &id_list, const bool save_tags = false);
|
||||
bool ResetPlayStatistics(const QStringList &id_str_list);
|
||||
void DeleteAll();
|
||||
void SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional<int> new_collection_directory_id);
|
||||
|
||||
SongList GetSongsBy(const QString &artist, const QString &album, const QString &title);
|
||||
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed);
|
||||
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
|
||||
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags = false);
|
||||
|
||||
void UpdateSongRating(const int id, const float rating, const bool save_tags = false);
|
||||
void UpdateSongsRating(const QList<int> &id_list, const float rating, const bool save_tags = false);
|
||||
@@ -248,12 +254,12 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
|
||||
|
||||
signals:
|
||||
void DirectoryDiscovered(Directory, SubdirectoryList);
|
||||
void DirectoryDeleted(Directory);
|
||||
void DirectoryDiscovered(CollectionDirectory, CollectionSubdirectoryList);
|
||||
void DirectoryDeleted(CollectionDirectory);
|
||||
|
||||
void SongsDiscovered(SongList);
|
||||
void SongsDeleted(SongList);
|
||||
void SongsStatisticsChanged(SongList);
|
||||
void SongsStatisticsChanged(SongList, bool = false);
|
||||
|
||||
void DatabaseReset();
|
||||
|
||||
@@ -278,9 +284,9 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
};
|
||||
|
||||
bool UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected);
|
||||
AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt = QueryOptions());
|
||||
SubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db);
|
||||
AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const CollectionFilterOptions &opt = CollectionFilterOptions());
|
||||
AlbumList GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt = CollectionFilterOptions());
|
||||
CollectionSubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db);
|
||||
|
||||
Song GetSongById(const int id, QSqlDatabase &db);
|
||||
SongList GetSongsById(const QStringList &ids, QSqlDatabase &db);
|
||||
|
||||
57
src/collection/collectiondirectory.h
Normal file
57
src/collection/collectiondirectory.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONDIRECTORY_H
|
||||
#define COLLECTIONDIRECTORY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
struct CollectionDirectory {
|
||||
CollectionDirectory() : id(-1) {}
|
||||
|
||||
bool operator==(const CollectionDirectory &other) const {
|
||||
return path == other.path && id == other.id;
|
||||
}
|
||||
|
||||
QString path;
|
||||
int id;
|
||||
};
|
||||
Q_DECLARE_METATYPE(CollectionDirectory)
|
||||
|
||||
using CollectionDirectoryList = QList<CollectionDirectory>;
|
||||
Q_DECLARE_METATYPE(CollectionDirectoryList)
|
||||
|
||||
struct CollectionSubdirectory {
|
||||
CollectionSubdirectory() : directory_id(-1), mtime(0) {}
|
||||
|
||||
int directory_id;
|
||||
QString path;
|
||||
qint64 mtime;
|
||||
};
|
||||
Q_DECLARE_METATYPE(CollectionSubdirectory)
|
||||
|
||||
using CollectionSubdirectoryList = QList<CollectionSubdirectory>;
|
||||
Q_DECLARE_METATYPE(CollectionSubdirectoryList)
|
||||
|
||||
#endif // COLLECTIONDIRECTORY_H
|
||||
@@ -29,8 +29,8 @@
|
||||
#include "core/filesystemmusicstorage.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/utilities.h"
|
||||
#include "directory.h"
|
||||
#include "utilities/diskutils.h"
|
||||
#include "collectiondirectory.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectiondirectorymodel.h"
|
||||
|
||||
@@ -46,17 +46,17 @@ CollectionDirectoryModel::CollectionDirectoryModel(CollectionBackend *backend, Q
|
||||
|
||||
CollectionDirectoryModel::~CollectionDirectoryModel() = default;
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) {
|
||||
void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &dir) {
|
||||
|
||||
QStandardItem *item = new QStandardItem(dir.path);
|
||||
item->setData(dir.id, kIdRole);
|
||||
item->setIcon(dir_icon_);
|
||||
storage_ << std::make_shared<FilesystemMusicStorage>(dir.path, dir.id);
|
||||
storage_ << std::make_shared<FilesystemMusicStorage>(backend_->source(), dir.path, dir.id);
|
||||
appendRow(item);
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDeleted(const Directory &dir) {
|
||||
void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir) {
|
||||
|
||||
for (int i = 0; i < rowCount(); ++i) {
|
||||
if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
|
||||
@@ -80,7 +80,7 @@ void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &idx) {
|
||||
|
||||
if (!backend_ || !idx.isValid()) return;
|
||||
|
||||
Directory dir;
|
||||
CollectionDirectory dir;
|
||||
dir.path = idx.data().toString();
|
||||
dir.id = idx.data(kIdRole).toInt();
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
class QModelIndex;
|
||||
|
||||
struct Directory;
|
||||
struct CollectionDirectory;
|
||||
class CollectionBackend;
|
||||
class MusicStorage;
|
||||
|
||||
@@ -53,8 +53,8 @@ class CollectionDirectoryModel : public QStandardItemModel {
|
||||
|
||||
private slots:
|
||||
// To be called by the backend
|
||||
void DirectoryDiscovered(const Directory &directories);
|
||||
void DirectoryDeleted(const Directory &directories);
|
||||
void DirectoryDiscovered(const CollectionDirectory &directories);
|
||||
void DirectoryDeleted(const CollectionDirectory &directories);
|
||||
|
||||
private:
|
||||
static const int kIdRole = Qt::UserRole + 1;
|
||||
|
||||
42
src/collection/collectionfilteroptions.cpp
Normal file
42
src/collection/collectionfilteroptions.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
#include "collectionfilteroptions.h"
|
||||
|
||||
CollectionFilterOptions::CollectionFilterOptions() : filter_mode_(FilterMode::All), max_age_(-1) {}
|
||||
|
||||
bool CollectionFilterOptions::Matches(const Song &song) const {
|
||||
|
||||
if (max_age_ != -1) {
|
||||
const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_;
|
||||
if (song.ctime() <= cutoff) return false;
|
||||
}
|
||||
|
||||
if (!filter_text_.isNull()) {
|
||||
return song.albumartist().contains(filter_text_, Qt::CaseInsensitive) || song.artist().contains(filter_text_, Qt::CaseInsensitive) || song.album().contains(filter_text_, Qt::CaseInsensitive) || song.title().contains(filter_text_, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
65
src/collection/collectionfilteroptions.h
Normal file
65
src/collection/collectionfilteroptions.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONFILTEROPTIONS_H
|
||||
#define COLLECTIONFILTEROPTIONS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionFilterOptions {
|
||||
public:
|
||||
|
||||
explicit CollectionFilterOptions();
|
||||
|
||||
// Filter mode:
|
||||
// - use the all songs table
|
||||
// - use the duplicated songs view; by duplicated we mean those songs for which the (artist, album, title) tuple is found more than once in the songs table
|
||||
// - use the untagged songs view; by untagged we mean those for which at least one of the (artist, album, title) tags is empty
|
||||
// Please note that additional filtering based on FTS table (the filter attribute) won't work in Duplicates and Untagged modes.
|
||||
enum class FilterMode {
|
||||
All,
|
||||
Duplicates,
|
||||
Untagged
|
||||
};
|
||||
|
||||
FilterMode filter_mode() const { return filter_mode_; }
|
||||
int max_age() const { return max_age_; }
|
||||
QString filter_text() const { return filter_text_; }
|
||||
|
||||
void set_filter_mode(const FilterMode filter_mode) {
|
||||
filter_mode_ = filter_mode;
|
||||
filter_text_.clear();
|
||||
}
|
||||
void set_max_age(const int max_age) { max_age_ = max_age; }
|
||||
void set_filter_text(const QString &filter_text) {
|
||||
filter_mode_ = FilterMode::All;
|
||||
filter_text_ = filter_text;
|
||||
}
|
||||
|
||||
bool Matches(const Song &song) const;
|
||||
|
||||
private:
|
||||
FilterMode filter_mode_;
|
||||
int max_age_;
|
||||
QString filter_text_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTEROPTIONS_H
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -45,6 +46,7 @@
|
||||
#include "core/iconloader.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionquery.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
@@ -52,16 +54,22 @@
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_collectionfilterwidget.h"
|
||||
#include "widgets/qsearchfield.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
#include "settings/appearancesettingspage.h"
|
||||
|
||||
CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_CollectionFilterWidget),
|
||||
model_(nullptr),
|
||||
group_by_dialog_(new GroupByDialog),
|
||||
group_by_dialog_(new GroupByDialog(this)),
|
||||
groupings_manager_(nullptr),
|
||||
filter_age_menu_(nullptr),
|
||||
group_by_menu_(nullptr),
|
||||
collection_menu_(nullptr),
|
||||
group_by_group_(nullptr),
|
||||
filter_delay_(new QTimer(this)),
|
||||
filter_applies_to_model_(true),
|
||||
delay_behaviour_(DelayedOnLargeLibraries) {
|
||||
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
@@ -114,13 +122,8 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
|
||||
filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
|
||||
|
||||
// "Group by ..."
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
|
||||
group_by_menu_ = new QMenu(tr("Group by"), this);
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
|
||||
QObject::connect(group_by_group_, &QActionGroup::triggered, this, &CollectionFilterWidget::GroupByClicked);
|
||||
QObject::connect(ui_->save_grouping, &QAction::triggered, this, &CollectionFilterWidget::SaveGroupBy);
|
||||
QObject::connect(ui_->manage_groupings, &QAction::triggered, this, &CollectionFilterWidget::ShowGroupingManager);
|
||||
|
||||
@@ -147,8 +150,8 @@ void CollectionFilterWidget::Init(CollectionModel *model) {
|
||||
|
||||
if (model_) {
|
||||
QObject::disconnect(model_, nullptr, this, nullptr);
|
||||
QObject::disconnect(model_, nullptr, group_by_dialog_.get(), nullptr);
|
||||
QObject::disconnect(group_by_dialog_.get(), nullptr, model_, nullptr);
|
||||
QObject::disconnect(model_, nullptr, group_by_dialog_, nullptr);
|
||||
QObject::disconnect(group_by_dialog_, nullptr, model_, nullptr);
|
||||
QList<QAction*> filter_ages = filter_ages_.keys();
|
||||
for (QAction *action : filter_ages) {
|
||||
QObject::disconnect(action, &QAction::triggered, model_, nullptr);
|
||||
@@ -158,9 +161,9 @@ void CollectionFilterWidget::Init(CollectionModel *model) {
|
||||
model_ = model;
|
||||
|
||||
// Connect signals
|
||||
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_.get(), &GroupByDialog::CollectionGroupingChanged);
|
||||
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged);
|
||||
QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged);
|
||||
QObject::connect(group_by_dialog_.get(), &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
|
||||
QObject::connect(group_by_dialog_, &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
|
||||
|
||||
QList<QAction*> filter_ages = filter_ages_.keys();
|
||||
for (QAction *action : filter_ages) {
|
||||
@@ -176,15 +179,32 @@ void CollectionFilterWidget::Init(CollectionModel *model) {
|
||||
if (s.contains(group_by_version())) version = s.value(group_by_version(), 0).toInt();
|
||||
if (version == 1) {
|
||||
model_->SetGroupBy(CollectionModel::Grouping(
|
||||
CollectionModel::GroupBy(s.value(group_by(1), static_cast<int>(CollectionModel::GroupBy_AlbumArtist)).toInt()),
|
||||
CollectionModel::GroupBy(s.value(group_by(2), static_cast<int>(CollectionModel::GroupBy_AlbumDisc)).toInt()),
|
||||
CollectionModel::GroupBy(s.value(group_by(3), static_cast<int>(CollectionModel::GroupBy_None)).toInt())));
|
||||
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(1), static_cast<int>(CollectionModel::GroupBy::AlbumArtist)).toInt()),
|
||||
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(2), static_cast<int>(CollectionModel::GroupBy::AlbumDisc)).toInt()),
|
||||
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(3), static_cast<int>(CollectionModel::GroupBy::None)).toInt())),
|
||||
s.value(separate_albums_by_grouping_key(), false).toBool());
|
||||
}
|
||||
else {
|
||||
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc, CollectionModel::GroupBy_None));
|
||||
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc, CollectionModel::GroupBy::None), false);
|
||||
}
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetSettingsGroup(const QString &settings_group) {
|
||||
|
||||
settings_group_ = settings_group;
|
||||
saved_groupings_settings_group_ = SavedGroupingManager::GetSavedGroupingsSettingsGroup(settings_group);
|
||||
|
||||
UpdateGroupByActions();
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) {
|
||||
|
||||
settings_prefix_ = prefix;
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ReloadSettings() {
|
||||
@@ -198,21 +218,10 @@ void CollectionFilterWidget::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionFilterWidget::group_by() {
|
||||
QString CollectionFilterWidget::group_by_version() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return QString("group_by");
|
||||
}
|
||||
else {
|
||||
return QString("%1_group_by").arg(settings_prefix_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString CollectionFilterWidget::group_by_version() {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return QString("group_by_version");
|
||||
return "group_by_version";
|
||||
}
|
||||
else {
|
||||
return QString("%1_group_by_version").arg(settings_prefix_);
|
||||
@@ -220,7 +229,29 @@ QString CollectionFilterWidget::group_by_version() {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionFilterWidget::group_by(const int number) { return group_by() + QString::number(number); }
|
||||
QString CollectionFilterWidget::group_by_key() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return "group_by";
|
||||
}
|
||||
else {
|
||||
return QString("%1_group_by").arg(settings_prefix_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString CollectionFilterWidget::group_by_key(const int number) const { return group_by_key() + QString::number(number); }
|
||||
|
||||
QString CollectionFilterWidget::separate_albums_by_grouping_key() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return "separate_albums_by_grouping";
|
||||
}
|
||||
else {
|
||||
return QString("%1_separate_albums_by_grouping").arg(settings_prefix_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::UpdateGroupByActions() {
|
||||
|
||||
@@ -229,7 +260,7 @@ void CollectionFilterWidget::UpdateGroupByActions() {
|
||||
delete group_by_group_;
|
||||
}
|
||||
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
group_by_group_ = CreateGroupByActions(saved_groupings_settings_group_, this);
|
||||
group_by_menu_->clear();
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
QObject::connect(group_by_group_, &QActionGroup::triggered, this, &CollectionFilterWidget::GroupByClicked);
|
||||
@@ -239,37 +270,36 @@ void CollectionFilterWidget::UpdateGroupByActions() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
QActionGroup *CollectionFilterWidget::CreateGroupByActions(QObject *parent) {
|
||||
QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent) {
|
||||
|
||||
QActionGroup *ret = new QActionGroup(parent);
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbumDisc)));
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_AlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::AlbumDisc)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbumDisc)));
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist)));
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Album)));
|
||||
|
||||
QAction *sep1 = new QAction(parent);
|
||||
sep1->setSeparator(true);
|
||||
ret->addAction(sep1);
|
||||
|
||||
// read saved groupings
|
||||
// Read saved groupings
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
s.beginGroup(saved_groupings_settings_group);
|
||||
int version = s.value("version").toInt();
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
@@ -306,7 +336,7 @@ QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObjec
|
||||
QAction *ret = new QAction(text, parent);
|
||||
ret->setCheckable(true);
|
||||
|
||||
if (grouping.first != CollectionModel::GroupBy_None) {
|
||||
if (grouping.first != CollectionModel::GroupBy::None) {
|
||||
ret->setProperty("group_by", QVariant::fromValue(grouping));
|
||||
}
|
||||
|
||||
@@ -316,20 +346,38 @@ QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObjec
|
||||
|
||||
void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
QString text = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (!text.isEmpty() && model_) {
|
||||
model_->SaveGrouping(text);
|
||||
UpdateGroupByActions();
|
||||
if (!model_) return;
|
||||
|
||||
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (name.isEmpty()) return;
|
||||
|
||||
qLog(Debug) << "Saving current grouping to" << name;
|
||||
|
||||
QSettings s;
|
||||
if (settings_group_.isEmpty() || settings_group_ == CollectionSettingsPage::kSettingsGroup) {
|
||||
s.beginGroup(SavedGroupingManager::kSavedGroupingsSettingsGroup);
|
||||
}
|
||||
else {
|
||||
s.beginGroup(QString(SavedGroupingManager::kSavedGroupingsSettingsGroup) + "_" + settings_group_);
|
||||
}
|
||||
QByteArray buffer;
|
||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||
datastream << model_->GetGroupBy();
|
||||
s.setValue("version", "1");
|
||||
s.setValue(name, buffer);
|
||||
s.endGroup();
|
||||
|
||||
UpdateGroupByActions();
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ShowGroupingManager() {
|
||||
|
||||
if (!groupings_manager_) {
|
||||
groupings_manager_ = std::make_unique<SavedGroupingManager>();
|
||||
groupings_manager_ = new SavedGroupingManager(saved_groupings_settings_group_, this);
|
||||
QObject::connect(groupings_manager_, &SavedGroupingManager::UpdateGroupByActions, this, &CollectionFilterWidget::UpdateGroupByActions);
|
||||
}
|
||||
groupings_manager_->SetFilter(this);
|
||||
|
||||
groupings_manager_->UpdateModel();
|
||||
groupings_manager_->show();
|
||||
|
||||
@@ -366,16 +414,16 @@ void CollectionFilterWidget::GroupByClicked(QAction *action) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g) {
|
||||
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping) {
|
||||
|
||||
if (!settings_group_.isEmpty()) {
|
||||
// Save the settings
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
s.setValue(group_by_version(), 1);
|
||||
s.setValue(group_by(1), static_cast<int>(g[0]));
|
||||
s.setValue(group_by(2), static_cast<int>(g[1]));
|
||||
s.setValue(group_by(3), static_cast<int>(g[2]));
|
||||
s.setValue(group_by_key(1), static_cast<int>(g[0]));
|
||||
s.setValue(group_by_key(2), static_cast<int>(g[1]));
|
||||
s.setValue(group_by_key(3), static_cast<int>(g[2]));
|
||||
s.setValue(separate_albums_by_grouping_key(), separate_albums_by_grouping);
|
||||
s.endGroup();
|
||||
}
|
||||
|
||||
@@ -386,6 +434,10 @@ void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g)
|
||||
|
||||
void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Grouping g) {
|
||||
|
||||
if (!group_by_group_) {
|
||||
UpdateGroupByActions();
|
||||
}
|
||||
|
||||
for (QAction *action : group_by_group_->actions()) {
|
||||
if (action->property("group_by").isNull()) continue;
|
||||
|
||||
@@ -406,12 +458,12 @@ void CollectionFilterWidget::SetFilterHint(const QString &hint) {
|
||||
ui_->search_field->setPlaceholderText(hint);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) {
|
||||
void CollectionFilterWidget::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) {
|
||||
|
||||
ui_->search_field->clear();
|
||||
ui_->search_field->setEnabled(query_mode == QueryOptions::QueryMode_All);
|
||||
ui_->search_field->setEnabled(filter_mode == CollectionFilterOptions::FilterMode::All);
|
||||
|
||||
model_->SetFilterQueryMode(query_mode);
|
||||
model_->SetFilterMode(filter_mode);
|
||||
|
||||
}
|
||||
|
||||
@@ -459,7 +511,7 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
||||
// Searching with one or two characters can be very expensive on the database even with FTS,
|
||||
// so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
|
||||
// so if the user is typing the first few characters of something it will be quicker.
|
||||
const bool delay = (delay_behaviour_ == AlwaysDelayed) || (delay_behaviour_ == DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
|
||||
if (delay) {
|
||||
filter_delay_->start();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -31,6 +32,7 @@
|
||||
#include <QString>
|
||||
|
||||
#include "collectionquery.h"
|
||||
#include "collectionqueryoptions.h"
|
||||
#include "collectionmodel.h"
|
||||
|
||||
class QTimer;
|
||||
@@ -52,7 +54,7 @@ class CollectionFilterWidget : public QWidget {
|
||||
|
||||
static const int kFilterDelay = 500; // msec
|
||||
|
||||
enum DelayBehaviour {
|
||||
enum class DelayBehaviour {
|
||||
AlwaysInstant,
|
||||
DelayedOnLargeLibraries,
|
||||
AlwaysDelayed,
|
||||
@@ -60,9 +62,8 @@ class CollectionFilterWidget : public QWidget {
|
||||
|
||||
void Init(CollectionModel *model);
|
||||
|
||||
static QActionGroup *CreateGroupByActions(QObject *parent);
|
||||
static QActionGroup *CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent);
|
||||
|
||||
void UpdateGroupByActions();
|
||||
void SetFilterHint(const QString &hint);
|
||||
void SetApplyFilterToCollection(bool filter_applies_to_model) { filter_applies_to_model_ = filter_applies_to_model; }
|
||||
void SetDelayBehaviour(DelayBehaviour behaviour) { delay_behaviour_ = behaviour; }
|
||||
@@ -73,12 +74,13 @@ class CollectionFilterWidget : public QWidget {
|
||||
QMenu *menu() const { return collection_menu_; }
|
||||
void AddMenuAction(QAction *action);
|
||||
|
||||
void SetSettingsGroup(const QString &group) { settings_group_ = group; }
|
||||
void SetSettingsPrefix(const QString &prefix) { settings_prefix_ = prefix; }
|
||||
void SetSettingsGroup(const QString &group);
|
||||
void SetSettingsPrefix(const QString &prefix);
|
||||
|
||||
QString group_by();
|
||||
QString group_by_version();
|
||||
QString group_by(const int number);
|
||||
QString group_by_version() const;
|
||||
QString group_by_key() const;
|
||||
QString group_by_key(const int number) const;
|
||||
QString separate_albums_by_grouping_key() const;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
@@ -86,7 +88,8 @@ class CollectionFilterWidget : public QWidget {
|
||||
void FocusSearchField();
|
||||
|
||||
public slots:
|
||||
void SetQueryMode(QueryOptions::QueryMode query_mode);
|
||||
void UpdateGroupByActions();
|
||||
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
|
||||
void FocusOnFilter(QKeyEvent *e);
|
||||
|
||||
signals:
|
||||
@@ -99,7 +102,7 @@ class CollectionFilterWidget : public QWidget {
|
||||
void keyReleaseEvent(QKeyEvent *e) override;
|
||||
|
||||
private slots:
|
||||
void GroupingChanged(const CollectionModel::Grouping g);
|
||||
void GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
|
||||
void GroupByClicked(QAction *action);
|
||||
void SaveGroupBy();
|
||||
void ShowGroupingManager();
|
||||
@@ -115,8 +118,8 @@ class CollectionFilterWidget : public QWidget {
|
||||
Ui_CollectionFilterWidget *ui_;
|
||||
CollectionModel *model_;
|
||||
|
||||
std::unique_ptr<GroupByDialog> group_by_dialog_;
|
||||
std::unique_ptr<SavedGroupingManager> groupings_manager_;
|
||||
GroupByDialog *group_by_dialog_;
|
||||
SavedGroupingManager *groupings_manager_;
|
||||
|
||||
QMenu *filter_age_menu_;
|
||||
QMenu *group_by_menu_;
|
||||
@@ -130,6 +133,7 @@ class CollectionFilterWidget : public QWidget {
|
||||
DelayBehaviour delay_behaviour_;
|
||||
|
||||
QString settings_group_;
|
||||
QString saved_groupings_settings_group_;
|
||||
QString settings_prefix_;
|
||||
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QAbstractItemModel>
|
||||
@@ -47,7 +49,9 @@
|
||||
#include "core/song.h"
|
||||
#include "core/sqlrow.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionquery.h"
|
||||
#include "collectionqueryoptions.h"
|
||||
#include "collectionitem.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
|
||||
@@ -64,8 +68,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
explicit CollectionModel(CollectionBackend *backend, Application *app, QObject *parent = nullptr);
|
||||
~CollectionModel() override;
|
||||
|
||||
static const char *kSavedGroupingsSettingsGroup;
|
||||
|
||||
static const int kPrettyCoverSize;
|
||||
static const char *kPixmapDiskCacheDir;
|
||||
|
||||
@@ -81,34 +83,34 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
};
|
||||
|
||||
// These values get saved in QSettings - don't change them
|
||||
enum GroupBy {
|
||||
GroupBy_None = 0,
|
||||
GroupBy_AlbumArtist = 1,
|
||||
GroupBy_Artist = 2,
|
||||
GroupBy_Album = 3,
|
||||
GroupBy_AlbumDisc = 4,
|
||||
GroupBy_YearAlbum = 5,
|
||||
GroupBy_YearAlbumDisc = 6,
|
||||
GroupBy_OriginalYearAlbum = 7,
|
||||
GroupBy_OriginalYearAlbumDisc = 8,
|
||||
GroupBy_Disc = 9,
|
||||
GroupBy_Year = 10,
|
||||
GroupBy_OriginalYear = 11,
|
||||
GroupBy_Genre = 12,
|
||||
GroupBy_Composer = 13,
|
||||
GroupBy_Performer = 14,
|
||||
GroupBy_Grouping = 15,
|
||||
GroupBy_FileType = 16,
|
||||
GroupBy_Format = 17,
|
||||
GroupBy_Samplerate = 18,
|
||||
GroupBy_Bitdepth = 19,
|
||||
GroupBy_Bitrate = 20,
|
||||
enum class GroupBy {
|
||||
None = 0,
|
||||
AlbumArtist = 1,
|
||||
Artist = 2,
|
||||
Album = 3,
|
||||
AlbumDisc = 4,
|
||||
YearAlbum = 5,
|
||||
YearAlbumDisc = 6,
|
||||
OriginalYearAlbum = 7,
|
||||
OriginalYearAlbumDisc = 8,
|
||||
Disc = 9,
|
||||
Year = 10,
|
||||
OriginalYear = 11,
|
||||
Genre = 12,
|
||||
Composer = 13,
|
||||
Performer = 14,
|
||||
Grouping = 15,
|
||||
FileType = 16,
|
||||
Format = 17,
|
||||
Samplerate = 18,
|
||||
Bitdepth = 19,
|
||||
Bitrate = 20,
|
||||
GroupByCount = 21,
|
||||
};
|
||||
Q_ENUM(GroupBy)
|
||||
|
||||
struct Grouping {
|
||||
explicit Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None, GroupBy t = GroupBy_None)
|
||||
explicit Grouping(GroupBy f = GroupBy::None, GroupBy s = GroupBy::None, GroupBy t = GroupBy::None)
|
||||
: first(f), second(s), third(t) {}
|
||||
|
||||
GroupBy first;
|
||||
@@ -160,9 +162,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
// Whether or not to show letters heading in the collection view
|
||||
void set_show_dividers(const bool show_dividers);
|
||||
|
||||
// Save the current grouping
|
||||
void SaveGrouping(const QString &name);
|
||||
|
||||
// Reload settings.
|
||||
void ReloadSettings();
|
||||
|
||||
@@ -182,9 +181,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
|
||||
|
||||
static bool IsArtistGroupBy(const GroupBy group_by) {
|
||||
return group_by == CollectionModel::GroupBy_Artist || group_by == CollectionModel::GroupBy_AlbumArtist;
|
||||
return group_by == CollectionModel::GroupBy::Artist || group_by == CollectionModel::GroupBy::AlbumArtist;
|
||||
}
|
||||
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy_Album || group_by == GroupBy_YearAlbum || group_by == GroupBy_AlbumDisc || group_by == GroupBy_YearAlbumDisc || group_by == GroupBy_OriginalYearAlbum || group_by == GroupBy_OriginalYearAlbumDisc; }
|
||||
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; }
|
||||
|
||||
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }
|
||||
|
||||
@@ -195,20 +194,20 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
void ExpandAll(CollectionItem *item = nullptr) const;
|
||||
|
||||
const CollectionModel::Grouping GetGroupBy() const { return group_by_; }
|
||||
void SetGroupBy(const CollectionModel::Grouping g);
|
||||
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
|
||||
|
||||
static QString ContainerKey(const GroupBy type, const Song &song);
|
||||
static QString ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song);
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void GroupingChanged(CollectionModel::Grouping g);
|
||||
void GroupingChanged(CollectionModel::Grouping g, bool separate_albums_by_grouping);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(const int age);
|
||||
void SetFilterText(const QString &text);
|
||||
void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
|
||||
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
|
||||
void SetFilterAge(const int filter_age);
|
||||
void SetFilterText(const QString &filter_text);
|
||||
|
||||
void Init(const bool async = true);
|
||||
void Reset();
|
||||
@@ -235,34 +234,35 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
|
||||
private:
|
||||
// Provides some optimisations for loading the list of items in the root.
|
||||
// Provides some optimizations for loading the list of items in the root.
|
||||
// This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread.
|
||||
QueryResult RunQuery(CollectionItem *parent);
|
||||
CollectionQueryOptions PrepareQuery(CollectionItem *parent);
|
||||
QueryResult RunQuery(const CollectionFilterOptions &filter_options = CollectionFilterOptions(), const CollectionQueryOptions &query_options = CollectionQueryOptions());
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal);
|
||||
|
||||
bool HasCompilations(const QSqlDatabase &db, const CollectionQuery &query);
|
||||
bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options);
|
||||
|
||||
void BeginReset();
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items.
|
||||
// Filters are added for each parent item, restricting the songs returned to a particular album or artist for example.
|
||||
static void InitQuery(const GroupBy type, CollectionQuery *q);
|
||||
static void FilterQuery(const GroupBy type, CollectionItem *item, CollectionQuery *q);
|
||||
static void SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options);
|
||||
static void AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
CollectionItem *ItemFromQuery(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
|
||||
CollectionItem *ItemFromSong(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
|
||||
CollectionItem *ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
|
||||
CollectionItem *ItemFromSong(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent);
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
CollectionItem *InitItem(const GroupBy type, const bool signal, CollectionItem *parent, const int container_level);
|
||||
void FinishItem(const GroupBy type, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level);
|
||||
void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
|
||||
static QString DividerKey(const GroupBy type, CollectionItem *item);
|
||||
static QString DividerDisplayText(const GroupBy type, const QString &key);
|
||||
static QString DividerKey(const GroupBy group_by, CollectionItem *item);
|
||||
static QString DividerDisplayText(const GroupBy group_by, const QString &key);
|
||||
|
||||
// Helpers
|
||||
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
|
||||
@@ -282,8 +282,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
QueryOptions query_options_;
|
||||
CollectionFilterOptions filter_options_;
|
||||
Grouping group_by_;
|
||||
bool separate_albums_by_grouping_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, CollectionItem*> song_nodes_;
|
||||
@@ -310,7 +311,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
AlbumCoverLoaderOptions cover_loader_options_;
|
||||
|
||||
typedef QPair<CollectionItem*, QString> ItemAndCacheKey;
|
||||
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
|
||||
QMap<quint64, ItemAndCacheKey> pending_art_;
|
||||
QSet<QString> pending_cache_keys_;
|
||||
};
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
|
||||
class SqlRow;
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source_Collection) {
|
||||
song_.set_source(Song::Source_Collection);
|
||||
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source::Collection) {
|
||||
song_.set_source(Song::Source::Collection);
|
||||
}
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source_Collection), song_(song) {
|
||||
song_.set_source(Song::Source_Collection);
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source::Collection), song_(song) {
|
||||
song_.set_source(Song::Source::Collection);
|
||||
}
|
||||
|
||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
||||
@@ -50,7 +50,7 @@ bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||
|
||||
// Rows from the songs tables come first
|
||||
song_.InitFromQuery(query, true);
|
||||
song_.set_source(Song::Source_Collection);
|
||||
song_.set_source(Song::Source::Collection);
|
||||
return song_.is_valid();
|
||||
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CollectionPlaylistItem : public PlaylistItem {
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(DatabaseColumn column) const override;
|
||||
Song DatabaseSongMetadata() const override { return Song(Song::Source_Collection); }
|
||||
Song DatabaseSongMetadata() const override { return Song(Song::Source::Collection); }
|
||||
|
||||
protected:
|
||||
Song song_;
|
||||
|
||||
@@ -31,16 +31,16 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/sqlquery.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include "collectionquery.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionqueryoptions.h"
|
||||
|
||||
QueryOptions::QueryOptions() : max_age_(-1), query_mode_(QueryMode_All) {}
|
||||
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options)
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
|
||||
: QSqlQuery(db),
|
||||
songs_table_(songs_table),
|
||||
fts_table_(fts_table),
|
||||
@@ -49,7 +49,7 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||
duplicates_only_(false),
|
||||
limit_(-1) {
|
||||
|
||||
if (!options.filter().isEmpty()) {
|
||||
if (!filter_options.filter_text().isEmpty()) {
|
||||
// We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5:
|
||||
// 1) Append * to all tokens.
|
||||
// 2) Prefix "fts" to column names.
|
||||
@@ -57,9 +57,9 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||
|
||||
// Split on whitespace
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList tokens(options.filter().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts));
|
||||
QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts));
|
||||
#else
|
||||
QStringList tokens(options.filter().split(QRegularExpression("\\s+"), QString::SkipEmptyParts));
|
||||
QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), QString::SkipEmptyParts));
|
||||
#endif
|
||||
QString query;
|
||||
for (QString token : tokens) {
|
||||
@@ -100,49 +100,40 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||
}
|
||||
}
|
||||
|
||||
if (options.max_age() != -1) {
|
||||
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - options.max_age();
|
||||
if (filter_options.max_age() != -1) {
|
||||
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age();
|
||||
|
||||
where_clauses_ << "ctime > ?";
|
||||
bound_values_ << cutoff;
|
||||
}
|
||||
|
||||
// TODO: Currently you cannot use any QueryMode other than All and FTS at the same time.
|
||||
// TODO: Currently you cannot use any FilterMode other than All and FTS at the same time.
|
||||
// Joining songs, duplicated_songs and songs_fts all together takes a huge amount of time.
|
||||
// The query takes about 20 seconds on my machine then. Why?
|
||||
// Untagged mode could work with additional filtering but I'm disabling it just to be consistent
|
||||
// this way filtering is available only in the All mode.
|
||||
// Remember though that when you fix the Duplicates + FTS cooperation, enable the filtering in both Duplicates and Untagged modes.
|
||||
duplicates_only_ = options.query_mode() == QueryOptions::QueryMode_Duplicates;
|
||||
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates;
|
||||
|
||||
if (options.query_mode() == QueryOptions::QueryMode_Untagged) {
|
||||
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
|
||||
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString CollectionQuery::GetInnerQuery() const {
|
||||
return duplicates_only_
|
||||
? QString(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
// Ignore 'literal' for IN
|
||||
if (op.compare("IN", Qt::CaseInsensitive) == 0) {
|
||||
QStringList values = value.toStringList();
|
||||
QStringList final;
|
||||
final.reserve(values.count());
|
||||
QStringList final_values;
|
||||
final_values.reserve(values.count());
|
||||
for (const QString &single_value : values) {
|
||||
final.append("?");
|
||||
final_values.append("?");
|
||||
bound_values_ << single_value;
|
||||
}
|
||||
|
||||
where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column);
|
||||
where_clauses_ << QString("%1 IN (" + final_values.join(",") + ")").arg(column);
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
|
||||
@@ -187,6 +178,15 @@ void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionQuery::GetInnerQuery() const {
|
||||
return duplicates_only_
|
||||
? QString(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
bool CollectionQuery::Exec() {
|
||||
|
||||
QString sql;
|
||||
@@ -213,32 +213,17 @@ bool CollectionQuery::Exec() {
|
||||
sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1));
|
||||
sql.replace("%fts_table", fts_table_);
|
||||
|
||||
prepare(sql);
|
||||
QSqlQuery::prepare(sql);
|
||||
|
||||
// Bind values
|
||||
for (const QVariant &value : bound_values_) {
|
||||
addBindValue(value);
|
||||
QSqlQuery::addBindValue(value);
|
||||
}
|
||||
|
||||
return exec();
|
||||
return QSqlQuery::exec();
|
||||
|
||||
}
|
||||
|
||||
bool CollectionQuery::Next() { return next(); }
|
||||
bool CollectionQuery::Next() { return QSqlQuery::next(); }
|
||||
|
||||
QVariant CollectionQuery::Value(const int column) const { return value(column); }
|
||||
|
||||
bool QueryOptions::Matches(const Song &song) const {
|
||||
|
||||
if (max_age_ != -1) {
|
||||
const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_;
|
||||
if (song.ctime() <= cutoff) return false;
|
||||
}
|
||||
|
||||
if (!filter_.isNull()) {
|
||||
return song.artist().contains(filter_, Qt::CaseInsensitive) || song.album().contains(filter_, Qt::CaseInsensitive) || song.title().contains(filter_, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
QVariant CollectionQuery::Value(const int column) const { return QSqlQuery::value(column); }
|
||||
|
||||
@@ -28,75 +28,23 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
|
||||
class Song;
|
||||
|
||||
// This structure let's you customize behaviour of any CollectionQuery.
|
||||
struct QueryOptions {
|
||||
// Modes of CollectionQuery:
|
||||
// - use the all songs table
|
||||
// - use the duplicated songs view; by duplicated we mean those songs for which the (artist, album, title) tuple is found more than once in the songs table
|
||||
// - use the untagged songs view; by untagged we mean those for which at least one of the (artist, album, title) tags is empty
|
||||
// Please note that additional filtering based on FTS table (the filter attribute) won't work in Duplicates and Untagged modes.
|
||||
enum QueryMode {
|
||||
QueryMode_All,
|
||||
QueryMode_Duplicates,
|
||||
QueryMode_Untagged
|
||||
};
|
||||
|
||||
QueryOptions();
|
||||
|
||||
bool Matches(const Song &song) const;
|
||||
|
||||
QString filter() const { return filter_; }
|
||||
void set_filter(const QString &filter) {
|
||||
filter_ = filter;
|
||||
query_mode_ = QueryMode_All;
|
||||
}
|
||||
|
||||
int max_age() const { return max_age_; }
|
||||
void set_max_age(int max_age) { max_age_ = max_age; }
|
||||
|
||||
QueryMode query_mode() const { return query_mode_; }
|
||||
void set_query_mode(QueryMode query_mode) {
|
||||
query_mode_ = query_mode;
|
||||
filter_ = QString();
|
||||
}
|
||||
|
||||
private:
|
||||
QString filter_;
|
||||
int max_age_;
|
||||
QueryMode query_mode_;
|
||||
};
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionqueryoptions.h"
|
||||
|
||||
class CollectionQuery : public QSqlQuery {
|
||||
public:
|
||||
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options = QueryOptions());
|
||||
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
|
||||
|
||||
// Sets contents of SELECT clause on the query (list of columns to get).
|
||||
void SetColumnSpec(const QString &spec) { column_spec_ = spec; }
|
||||
|
||||
// Sets an ORDER BY clause on the query.
|
||||
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
||||
|
||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
|
||||
// Please note that IN operator expects a QStringList as value.
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||
void AddWhereArtist(const QVariant &value);
|
||||
|
||||
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
|
||||
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
|
||||
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
|
||||
void SetIncludeUnavailable(const bool include_unavailable) { include_unavailable_ = include_unavailable; }
|
||||
void SetLimit(const int limit) { limit_ = limit; }
|
||||
void AddCompilationRequirement(const bool compilation);
|
||||
QVariant Value(const int column) const;
|
||||
QVariant value(const int column) const { return Value(column); }
|
||||
|
||||
bool Exec();
|
||||
bool exec() { return QSqlQuery::exec(); }
|
||||
|
||||
bool Next();
|
||||
QVariant Value(const int column) const;
|
||||
|
||||
QString column_spec() const { return column_spec_; }
|
||||
QString order_by() const { return order_by_; }
|
||||
@@ -107,6 +55,24 @@ class CollectionQuery : public QSqlQuery {
|
||||
bool duplicates_only() const { return duplicates_only_; }
|
||||
int limit() const { return limit_; }
|
||||
|
||||
// Sets contents of SELECT clause on the query (list of columns to get).
|
||||
void SetColumnSpec(const QString &column_spec) { column_spec_ = column_spec; }
|
||||
|
||||
// Sets an ORDER BY clause on the query.
|
||||
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
||||
|
||||
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
|
||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
|
||||
// Please note that IN operator expects a QStringList as value.
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||
void AddWhereArtist(const QVariant &value);
|
||||
|
||||
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
|
||||
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
|
||||
void SetIncludeUnavailable(const bool include_unavailable) { include_unavailable_ = include_unavailable; }
|
||||
void SetLimit(const int limit) { limit_ = limit; }
|
||||
void AddCompilationRequirement(const bool compilation);
|
||||
|
||||
private:
|
||||
QString GetInnerQuery() const;
|
||||
|
||||
|
||||
34
src/collection/collectionqueryoptions.cpp
Normal file
34
src/collection/collectionqueryoptions.cpp
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "collectionqueryoptions.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
|
||||
CollectionQueryOptions::CollectionQueryOptions()
|
||||
: compilation_requirement_(CollectionQueryOptions::CompilationRequirement::None),
|
||||
query_have_compilations_(false) {}
|
||||
|
||||
void CollectionQueryOptions::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
where_clauses_ << Where(column, value, op);
|
||||
|
||||
}
|
||||
63
src/collection/collectionqueryoptions.h
Normal file
63
src/collection/collectionqueryoptions.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONQUERYOPTIONS_H
|
||||
#define COLLECTIONQUERYOPTIONS_H
|
||||
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
class CollectionQueryOptions {
|
||||
public:
|
||||
|
||||
explicit CollectionQueryOptions();
|
||||
|
||||
struct Where {
|
||||
explicit Where(const QString _column = QString(), const QVariant _value = QString(), const QString _op = QString()) : column(_column), value(_value), op(_op) {}
|
||||
QString column;
|
||||
QVariant value;
|
||||
QString op;
|
||||
};
|
||||
|
||||
enum class CompilationRequirement {
|
||||
None,
|
||||
On,
|
||||
Off
|
||||
};
|
||||
|
||||
QString column_spec() const { return column_spec_; }
|
||||
CompilationRequirement compilation_requirement() const { return compilation_requirement_; }
|
||||
bool query_have_compilations() const { return query_have_compilations_; }
|
||||
|
||||
void set_column_spec(const QString &column_spec) { column_spec_ = column_spec; }
|
||||
void set_compilation_requirement(const CompilationRequirement compilation_requirement) { compilation_requirement_ = compilation_requirement; }
|
||||
void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; }
|
||||
|
||||
QList<Where> where_clauses() const { return where_clauses_; }
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||
|
||||
private:
|
||||
QString column_spec_;
|
||||
CompilationRequirement compilation_requirement_;
|
||||
bool query_have_compilations_;
|
||||
QList<Where> where_clauses_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONQUERYOPTIONS_H
|
||||
@@ -50,9 +50,9 @@
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/deletefiles.h"
|
||||
#include "utilities/filemanagerutils.h"
|
||||
#include "collection.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectiondirectorymodel.h"
|
||||
@@ -699,7 +699,7 @@ void CollectionView::DeleteFilesFinished(const SongList &songs_with_errors) {
|
||||
if (songs_with_errors.isEmpty()) return;
|
||||
|
||||
OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this);
|
||||
dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors);
|
||||
dialog->Show(OrganizeErrorDialog::OperationType::Delete, songs_with_errors);
|
||||
// It deletes itself when the user closes it
|
||||
|
||||
}
|
||||
|
||||
@@ -38,21 +38,19 @@
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/filesystemwatcherinterface.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/imageutils.h"
|
||||
#include "directory.h"
|
||||
#include "utilities/imageutils.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "collectiondirectory.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectionwatcher.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
@@ -80,13 +78,12 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
|
||||
scan_on_startup_(true),
|
||||
monitor_(true),
|
||||
song_tracking_(false),
|
||||
mark_songs_unavailable_(source_ == Song::Source_Collection),
|
||||
mark_songs_unavailable_(source_ == Song::Source::Collection),
|
||||
expire_unavailable_songs_days_(60),
|
||||
overwrite_playcount_(false),
|
||||
overwrite_rating_(false),
|
||||
stop_requested_(false),
|
||||
abort_requested_(false),
|
||||
rescan_in_progress_(false),
|
||||
rescan_timer_(new QTimer(this)),
|
||||
periodic_scan_timer_(new QTimer(this)),
|
||||
rescan_paused_(false),
|
||||
@@ -145,7 +142,7 @@ void CollectionWatcher::ReloadSettings() {
|
||||
scan_on_startup_ = s.value("startup_scan", true).toBool();
|
||||
monitor_ = s.value("monitor", true).toBool();
|
||||
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
|
||||
if (source_ == Song::Source_Collection) {
|
||||
if (source_ == Song::Source::Collection) {
|
||||
song_tracking_ = s.value("song_tracking", false).toBool();
|
||||
mark_songs_unavailable_ = song_tracking_ ? true : s.value("mark_songs_unavailable", true).toBool();
|
||||
}
|
||||
@@ -169,15 +166,15 @@ void CollectionWatcher::ReloadSettings() {
|
||||
}
|
||||
else if (monitor_ && !was_monitoring_before) {
|
||||
// Add all directories to all QFileSystemWatchers again
|
||||
for (const Directory &dir : std::as_const(watched_dirs_)) {
|
||||
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
|
||||
for (const Subdirectory &subdir : subdirs) {
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
CollectionSubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
AddWatch(dir, subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mark_songs_unavailable_ && !periodic_scan_timer_->isActive()) {
|
||||
if (monitor_ && scan_on_startup_ && mark_songs_unavailable_ && !periodic_scan_timer_->isActive()) {
|
||||
periodic_scan_timer_->start();
|
||||
}
|
||||
else if (!mark_songs_unavailable_ && periodic_scan_timer_->isActive()) {
|
||||
@@ -241,7 +238,7 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
|
||||
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
|
||||
if (!deleted_songs.isEmpty()) {
|
||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source_Collection) {
|
||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
||||
emit watcher_->SongsUnavailable(deleted_songs);
|
||||
}
|
||||
else {
|
||||
@@ -274,7 +271,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
touched_subdirs.clear();
|
||||
}
|
||||
|
||||
for (const Subdirectory &subdir : deleted_subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : deleted_subdirs) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
||||
}
|
||||
@@ -283,7 +280,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
|
||||
if (watcher_->monitor_) {
|
||||
// Watch the new subdirectories
|
||||
for (const Subdirectory &subdir : new_subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : new_subdirs) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
}
|
||||
@@ -331,7 +328,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const SubdirectoryList &subdirs) {
|
||||
void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdirectoryList &subdirs) {
|
||||
|
||||
known_subdirs_ = subdirs;
|
||||
known_subdirs_dirty_ = false;
|
||||
@@ -344,18 +341,18 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
}
|
||||
|
||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const Subdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
}
|
||||
|
||||
SubdirectoryList ret;
|
||||
for (const Subdirectory &subdir : known_subdirs_) {
|
||||
CollectionSubdirectoryList ret;
|
||||
for (const CollectionSubdirectory &subdir : known_subdirs_) {
|
||||
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path && subdir.mtime != 0) {
|
||||
ret << subdir;
|
||||
}
|
||||
@@ -365,7 +362,7 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const Q
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
@@ -375,7 +372,7 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryList &subdirs) {
|
||||
void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs) {
|
||||
|
||||
stop_requested_ = false;
|
||||
|
||||
@@ -387,7 +384,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
|
||||
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(files_count);
|
||||
ScanSubdirectory(dir.path, Subdirectory(), files_count, &transaction);
|
||||
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
|
||||
}
|
||||
else {
|
||||
@@ -397,7 +394,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
|
||||
const quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(files_count);
|
||||
for (const Subdirectory &subdir : subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
|
||||
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
@@ -413,14 +410,14 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
|
||||
QFileInfo path_info(path);
|
||||
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
if (path_info.isSymLink()) {
|
||||
QString real_path = path_info.symLinkTarget();
|
||||
for (const Directory &dir : std::as_const(watched_dirs_)) {
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
return;
|
||||
}
|
||||
@@ -442,12 +439,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
|
||||
|
||||
QMap<QString, QStringList> album_art;
|
||||
QStringList files_on_disk;
|
||||
SubdirectoryList my_new_subdirs;
|
||||
CollectionSubdirectoryList my_new_subdirs;
|
||||
|
||||
// If a directory is moved then only its parent gets a changed notification, so we need to look and see if any of our children don't exist any more.
|
||||
// If a directory is moved then only its parent gets a changed notification, so we need to look and see if any of our children don't exist anymore.
|
||||
// If one has been removed, "rescan" it to get the deleted songs
|
||||
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const Subdirectory &prev_subdir : previous_subdirs) {
|
||||
CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
||||
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
||||
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
|
||||
}
|
||||
@@ -464,8 +461,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
|
||||
|
||||
if (child_info.isDir()) {
|
||||
if (!t->HasSeenSubdir(child)) {
|
||||
// We haven't seen this subdirectory before - add it to a list and later we'll tell the backend about it and scan it.
|
||||
Subdirectory new_subdir;
|
||||
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
|
||||
CollectionSubdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child;
|
||||
new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch();
|
||||
@@ -678,7 +675,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
|
||||
}
|
||||
|
||||
// Add this subdir to the new or touched list
|
||||
Subdirectory updated_subdir;
|
||||
CollectionSubdirectory updated_subdir;
|
||||
updated_subdir.directory_id = t->dir();
|
||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
updated_subdir.path = path;
|
||||
@@ -690,12 +687,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
|
||||
t->touched_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
if (updated_subdir.mtime == 0) { // Subdirectory deleted, mark it for removal from the watcher.
|
||||
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
// Recurse into the new subdirs that we found
|
||||
for (const Subdirectory &my_new_subdir : my_new_subdirs) {
|
||||
for (const CollectionSubdirectory &my_new_subdir : my_new_subdirs) {
|
||||
if (stop_requested_ || abort_requested_) return;
|
||||
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
}
|
||||
@@ -761,11 +758,11 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const bool cue_deleted,
|
||||
ScanTransaction *t) {
|
||||
|
||||
// If a CUE got deleted, we turn it's first section into the new 'raw' (cueless) song and we just remove the rest of the sections from the collection
|
||||
// If a CUE got deleted, we turn it's first section into the new 'raw' (cueless) song, and we just remove the rest of the sections from the collection
|
||||
const Song &matching_song = matching_songs.first();
|
||||
if (cue_deleted) {
|
||||
for (const Song &song : matching_songs) {
|
||||
if (!song.IsMetadataAndMoreEqual(matching_song)) {
|
||||
if (!song.IsAllMetadataEqual(matching_song)) {
|
||||
t->deleted_songs << song;
|
||||
}
|
||||
}
|
||||
@@ -805,7 +802,7 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path
|
||||
|
||||
// Ignore FILEs pointing to other media files.
|
||||
// Also, watch out for incorrect media files.
|
||||
// Playlist parser for CUEs considers every entry in sheet valid and we don't want invalid media getting into collection!
|
||||
// Playlist parser for CUEs considers every entry in sheet valid, and we don't want invalid media getting into collection!
|
||||
QString file_nfd = file.normalized(QString::NormalizationForm_D);
|
||||
SongList cue_congs = cue_parser_->Load(&cue_file, matching_cue, path, false);
|
||||
cue_file.close();
|
||||
@@ -857,10 +854,26 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
|
||||
changes << "metadata";
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.art_automatic() != new_song.art_automatic() || matching_song.art_manual() != new_song.art_manual()) {
|
||||
if (!matching_song.IsPlayStatisticsEqual(new_song)) {
|
||||
changes << "play statistics";
|
||||
notify_new = true;
|
||||
}
|
||||
if (!matching_song.IsRatingEqual(new_song)) {
|
||||
changes << "rating";
|
||||
notify_new = true;
|
||||
}
|
||||
if (!matching_song.IsArtEqual(new_song)) {
|
||||
changes << "album art";
|
||||
notify_new = true;
|
||||
}
|
||||
if (!matching_song.IsAcoustIdEqual(new_song)) {
|
||||
changes << "acoustid";
|
||||
notify_new = true;
|
||||
}
|
||||
if (!matching_song.IsMusicBrainzEqual(new_song)) {
|
||||
changes << "musicbrainz";
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.mtime() != new_song.mtime()) {
|
||||
changes << "mtime";
|
||||
}
|
||||
@@ -899,7 +912,7 @@ quint64 CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
|
||||
return cue_last_modified.isValid() ? cue_last_modified.toSecsSinceEpoch() : 0;
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) {
|
||||
void CollectionWatcher::AddWatch(const CollectionDirectory &dir, const QString &path) {
|
||||
|
||||
if (!QFile::exists(path)) return;
|
||||
|
||||
@@ -909,7 +922,7 @@ void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RemoveWatch(const Directory &dir, const Subdirectory &subdir) {
|
||||
void CollectionWatcher::RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir) {
|
||||
|
||||
QStringList subdir_paths = subdir_mapping_.keys(dir);
|
||||
for (const QString &subdir_path : subdir_paths) {
|
||||
@@ -921,7 +934,7 @@ void CollectionWatcher::RemoveWatch(const Directory &dir, const Subdirectory &su
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RemoveDirectory(const Directory &dir) {
|
||||
void CollectionWatcher::RemoveDirectory(const CollectionDirectory &dir) {
|
||||
|
||||
rescan_queue_.remove(dir.id);
|
||||
watched_dirs_.remove(dir.id);
|
||||
@@ -981,11 +994,11 @@ bool CollectionWatcher::FindSongsByFingerprint(const QString &file, const SongLi
|
||||
void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
// Find what dir it was in
|
||||
QHash<QString, Directory>::const_iterator it = subdir_mapping_.constFind(subdir);
|
||||
QHash<QString, CollectionDirectory>::const_iterator it = subdir_mapping_.constFind(subdir);
|
||||
if (it == subdir_mapping_.constEnd()) {
|
||||
return;
|
||||
}
|
||||
Directory dir = *it;
|
||||
CollectionDirectory dir = *it;
|
||||
|
||||
qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path << "id" << dir.id;
|
||||
|
||||
@@ -1012,7 +1025,7 @@ void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
for (const QString &path : rescan_queue_[dir]) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
Subdirectory subdir;
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.directory_id = dir;
|
||||
subdir.mtime = 0;
|
||||
subdir.path = path;
|
||||
@@ -1115,18 +1128,6 @@ void CollectionWatcher::FullScanAsync() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RescanTracksAsync(const SongList &songs) {
|
||||
|
||||
// Is List thread safe? if not, this may crash.
|
||||
song_rescan_queue_.append(songs);
|
||||
|
||||
// Call only if it's not already running
|
||||
if (!rescan_in_progress_) {
|
||||
QMetaObject::invokeMethod(this, "RescanTracksNow", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::IncrementalScanCheck() {
|
||||
|
||||
qint64 duration = QDateTime::currentDateTime().toSecsSinceEpoch() - last_scan_time_;
|
||||
@@ -1141,48 +1142,20 @@ void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
|
||||
|
||||
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
|
||||
|
||||
void CollectionWatcher::RescanTracksNow() {
|
||||
|
||||
Q_ASSERT(!rescan_in_progress_);
|
||||
stop_requested_ = false;
|
||||
|
||||
// Currently we are too stupid to rescan one file at a time, so we'll just scan the full directories
|
||||
QStringList scanned_dirs; // To avoid double scans
|
||||
while (!song_rescan_queue_.isEmpty()) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
Song song = song_rescan_queue_.takeFirst();
|
||||
QString songdir = song.url().toLocalFile().section('/', 0, -2);
|
||||
if (!scanned_dirs.contains(songdir)) {
|
||||
qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir;
|
||||
ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_);
|
||||
quint64 files_count = FilesCountForPath(&transaction, songdir);
|
||||
ScanSubdirectory(songdir, Subdirectory(), files_count, &transaction);
|
||||
scanned_dirs << songdir;
|
||||
emit CompilationsNeedUpdating();
|
||||
}
|
||||
else {
|
||||
qLog(Debug) << "Directory" << songdir << "already scanned - skipping.";
|
||||
}
|
||||
}
|
||||
Q_ASSERT(song_rescan_queue_.isEmpty());
|
||||
rescan_in_progress_ = false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mtimes) {
|
||||
|
||||
stop_requested_ = false;
|
||||
|
||||
for (const Directory &dir : std::as_const(watched_dirs_)) {
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
|
||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
||||
SubdirectoryList subdirs(transaction.GetAllSubdirs());
|
||||
CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
|
||||
|
||||
if (subdirs.isEmpty()) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
||||
Subdirectory subdir;
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.path = dir.path;
|
||||
subdir.directory_id = dir.id;
|
||||
subdirs << subdir;
|
||||
@@ -1192,7 +1165,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
||||
quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
|
||||
transaction.AddToProgressMax(files_count);
|
||||
|
||||
for (const Subdirectory &subdir : subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
@@ -1219,7 +1192,7 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &
|
||||
if (path_info.isDir()) {
|
||||
if (path_info.isSymLink()) {
|
||||
QString real_path = path_info.symLinkTarget();
|
||||
for (const Directory &dir : std::as_const(watched_dirs_)) {
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1241,10 +1214,10 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &
|
||||
|
||||
}
|
||||
|
||||
quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap<QString, quint64> &subdir_files_count) {
|
||||
quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap<QString, quint64> &subdir_files_count) {
|
||||
|
||||
quint64 i = 0;
|
||||
for (const Subdirectory &subdir : subdirs) {
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
const quint64 files_count = FilesCountForPath(t, subdir.path);
|
||||
subdir_files_count[subdir.path] = files_count;
|
||||
@@ -1254,3 +1227,34 @@ quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const Subdir
|
||||
return i;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RescanSongsAsync(const SongList &songs) {
|
||||
|
||||
QMetaObject::invokeMethod(this, "RescanSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RescanSongs(const SongList &songs) {
|
||||
|
||||
stop_requested_ = false;
|
||||
|
||||
QStringList scanned_paths;
|
||||
for (const Song &song : songs) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
const QString song_path = song.url().toLocalFile().section('/', 0, -2);
|
||||
if (scanned_paths.contains(song_path)) continue;
|
||||
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
||||
CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_requested_ || abort_requested_) break;
|
||||
if (subdir.path != song_path) continue;
|
||||
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
||||
quint64 files_count = FilesCountForPath(&transaction, subdir.path);
|
||||
ScanSubdirectory(song_path, subdir, files_count, &transaction);
|
||||
scanned_paths << subdir.path;
|
||||
}
|
||||
}
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "directory.h"
|
||||
#include "collectiondirectory.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class QThread;
|
||||
@@ -59,7 +59,6 @@ class CollectionWatcher : public QObject {
|
||||
|
||||
void IncrementalScanAsync();
|
||||
void FullScanAsync();
|
||||
void RescanTracksAsync(const SongList &songs);
|
||||
void SetRescanPausedAsync(const bool pause);
|
||||
void ReloadSettingsAsync();
|
||||
|
||||
@@ -68,14 +67,16 @@ class CollectionWatcher : public QObject {
|
||||
|
||||
void ExitAsync();
|
||||
|
||||
void RescanSongsAsync(const SongList &songs);
|
||||
|
||||
signals:
|
||||
void NewOrUpdatedSongs(SongList);
|
||||
void SongsMTimeUpdated(SongList);
|
||||
void SongsDeleted(SongList);
|
||||
void SongsUnavailable(SongList songs, bool unavailable = true);
|
||||
void SongsReadded(SongList songs, bool unavailable = false);
|
||||
void SubdirsDiscovered(SubdirectoryList subdirs);
|
||||
void SubdirsMTimeUpdated(SubdirectoryList subdirs);
|
||||
void SubdirsDiscovered(CollectionSubdirectoryList subdirs);
|
||||
void SubdirsMTimeUpdated(CollectionSubdirectoryList subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days);
|
||||
void ExitFinished();
|
||||
@@ -83,8 +84,8 @@ class CollectionWatcher : public QObject {
|
||||
void ScanStarted(int task_id);
|
||||
|
||||
public slots:
|
||||
void AddDirectory(const Directory &dir, const SubdirectoryList &subdirs);
|
||||
void RemoveDirectory(const Directory &dir);
|
||||
void AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs);
|
||||
void RemoveDirectory(const CollectionDirectory &dir);
|
||||
void SetRescanPaused(bool pause);
|
||||
|
||||
private:
|
||||
@@ -102,9 +103,9 @@ class CollectionWatcher : public QObject {
|
||||
SongList FindSongsInSubdirectory(const QString &path);
|
||||
bool HasSongsWithMissingFingerprint(const QString &path);
|
||||
bool HasSeenSubdir(const QString &path);
|
||||
void SetKnownSubdirs(const SubdirectoryList &subdirs);
|
||||
SubdirectoryList GetImmediateSubdirs(const QString &path);
|
||||
SubdirectoryList GetAllSubdirs();
|
||||
void SetKnownSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
CollectionSubdirectoryList GetImmediateSubdirs(const QString &path);
|
||||
CollectionSubdirectoryList GetAllSubdirs();
|
||||
|
||||
void AddToProgress(const quint64 n = 1);
|
||||
void AddToProgressMax(const quint64 n);
|
||||
@@ -120,9 +121,9 @@ class CollectionWatcher : public QObject {
|
||||
SongList readded_songs;
|
||||
SongList new_songs;
|
||||
SongList touched_songs;
|
||||
SubdirectoryList new_subdirs;
|
||||
SubdirectoryList touched_subdirs;
|
||||
SubdirectoryList deleted_subdirs;
|
||||
CollectionSubdirectoryList new_subdirs;
|
||||
CollectionSubdirectoryList touched_subdirs;
|
||||
CollectionSubdirectoryList deleted_subdirs;
|
||||
|
||||
QStringList files_changed_path_;
|
||||
|
||||
@@ -155,7 +156,7 @@ class CollectionWatcher : public QObject {
|
||||
QMultiMap<QString, Song> cached_songs_missing_fingerprint_;
|
||||
bool cached_songs_missing_fingerprint_dirty_;
|
||||
|
||||
SubdirectoryList known_subdirs_;
|
||||
CollectionSubdirectoryList known_subdirs_;
|
||||
bool known_subdirs_dirty_;
|
||||
};
|
||||
|
||||
@@ -166,9 +167,9 @@ class CollectionWatcher : public QObject {
|
||||
void IncrementalScanCheck();
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanTracksNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void RescanSongs(const SongList &songs);
|
||||
|
||||
private:
|
||||
static bool FindSongsByPath(const SongList &songs, const QString &path, SongList *out);
|
||||
@@ -179,8 +180,8 @@ class CollectionWatcher : public QObject {
|
||||
inline static QString DirectoryPart(const QString &fileName);
|
||||
QString PickBestImage(const QStringList &images);
|
||||
QUrl ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
|
||||
void AddWatch(const Directory &dir, const QString &path);
|
||||
void RemoveWatch(const Directory &dir, const Subdirectory &subdir);
|
||||
void AddWatch(const CollectionDirectory &dir, const QString &path);
|
||||
void RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir);
|
||||
static quint64 GetMtimeForCue(const QString &cue_path);
|
||||
void PerformScan(const bool incremental, const bool ignore_mtimes);
|
||||
|
||||
@@ -195,7 +196,7 @@ class CollectionWatcher : public QObject {
|
||||
static void AddChangedSong(const QString &file, const Song &matching_song, const Song &new_song, ScanTransaction *t);
|
||||
|
||||
quint64 FilesCountForPath(ScanTransaction *t, const QString &path);
|
||||
quint64 FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap<QString, quint64> &subdir_files_count);
|
||||
quint64 FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap<QString, quint64> &subdir_files_count);
|
||||
|
||||
QString FindCueFilename(const QString &filename);
|
||||
|
||||
@@ -207,7 +208,7 @@ class CollectionWatcher : public QObject {
|
||||
|
||||
FileSystemWatcherInterface *fs_watcher_;
|
||||
QThread *original_thread_;
|
||||
QHash<QString, Directory> subdir_mapping_;
|
||||
QHash<QString, CollectionDirectory> subdir_mapping_;
|
||||
|
||||
// A list of words use to try to identify the (likely) best image found in an directory to use as cover artwork.
|
||||
// e.g. using ["front", "cover"] would identify front.jpg and exclude back.jpg.
|
||||
@@ -223,9 +224,8 @@ class CollectionWatcher : public QObject {
|
||||
|
||||
bool stop_requested_;
|
||||
bool abort_requested_;
|
||||
bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working.
|
||||
|
||||
QMap<int, Directory> watched_dirs_;
|
||||
QMap<int, CollectionDirectory> watched_dirs_;
|
||||
QTimer *rescan_timer_;
|
||||
QTimer *periodic_scan_timer_;
|
||||
QMap<int, QStringList> rescan_queue_; // dir id -> list of subdirs to be scanned
|
||||
@@ -237,8 +237,6 @@ class CollectionWatcher : public QObject {
|
||||
|
||||
static QStringList sValidImages;
|
||||
|
||||
SongList song_rescan_queue_; // Set by UI thread
|
||||
|
||||
qint64 last_scan_time_;
|
||||
|
||||
};
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DIRECTORY_H
|
||||
#define DIRECTORY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QSqlQuery>
|
||||
|
||||
struct Directory {
|
||||
Directory() : id(-1) {}
|
||||
|
||||
bool operator==(const Directory &other) const {
|
||||
return path == other.path && id == other.id;
|
||||
}
|
||||
|
||||
QString path;
|
||||
int id;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Directory)
|
||||
|
||||
typedef QList<Directory> DirectoryList;
|
||||
Q_DECLARE_METATYPE(DirectoryList)
|
||||
|
||||
|
||||
struct Subdirectory {
|
||||
Subdirectory() : directory_id(-1), mtime(0) {}
|
||||
|
||||
int directory_id;
|
||||
QString path;
|
||||
qint64 mtime;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Subdirectory)
|
||||
|
||||
typedef QList<Subdirectory> SubdirectoryList;
|
||||
Q_DECLARE_METATYPE(SubdirectoryList)
|
||||
|
||||
#endif // DIRECTORY_H
|
||||
|
||||
|
||||
@@ -63,11 +63,7 @@ struct tag_group_by {};
|
||||
|
||||
class GroupByDialogPrivate {
|
||||
private:
|
||||
typedef multi_index_container<
|
||||
Mapping,
|
||||
indexed_by<
|
||||
ordered_unique<tag<tag_index>, member<Mapping, int, &Mapping::combo_box_index> >,
|
||||
ordered_unique<tag<tag_group_by>, member<Mapping, CollectionModel::GroupBy, &Mapping::group_by> > > > MappingContainer;
|
||||
using MappingContainer = multi_index_container<Mapping, indexed_by<ordered_unique<tag<tag_index>, member<Mapping, int, &Mapping::combo_box_index>>, ordered_unique<tag<tag_group_by>, member<Mapping, CollectionModel::GroupBy, &Mapping::group_by>>>>;
|
||||
|
||||
public:
|
||||
MappingContainer mapping_;
|
||||
@@ -78,26 +74,26 @@ GroupByDialog::GroupByDialog(QWidget *parent) : QDialog(parent), ui_(new Ui_Grou
|
||||
ui_->setupUi(this);
|
||||
Reset();
|
||||
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_None, 0));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Artist, 1));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumArtist, 2));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Album, 3));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumDisc, 4));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Disc, 5));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Format, 6));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Genre, 7));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Year, 8));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbum, 9));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbumDisc, 10));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYear, 11));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYearAlbum, 12));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Composer, 13));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Performer, 14));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Grouping, 15));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_FileType, 16));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Samplerate, 17));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitdepth, 18));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitrate, 19));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::None, 0));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Artist, 1));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::AlbumArtist, 2));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Album, 3));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::AlbumDisc, 4));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Disc, 5));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Format, 6));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Genre, 7));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Year, 8));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::YearAlbum, 9));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::YearAlbumDisc, 10));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::OriginalYear, 11));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::OriginalYearAlbum, 12));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Composer, 13));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Performer, 14));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Grouping, 15));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::FileType, 16));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Samplerate, 17));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Bitdepth, 18));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Bitrate, 19));
|
||||
|
||||
QObject::connect(ui_->buttonbox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &GroupByDialog::Reset);
|
||||
|
||||
@@ -108,22 +104,31 @@ GroupByDialog::GroupByDialog(QWidget *parent) : QDialog(parent), ui_(new Ui_Grou
|
||||
GroupByDialog::~GroupByDialog() = default;
|
||||
|
||||
void GroupByDialog::Reset() {
|
||||
|
||||
ui_->combobox_first->setCurrentIndex(2); // Album Artist
|
||||
ui_->combobox_second->setCurrentIndex(3); // Album
|
||||
ui_->combobox_second->setCurrentIndex(4); // Album Disc
|
||||
ui_->combobox_third->setCurrentIndex(0); // None
|
||||
ui_->checkbox_separate_albums_by_grouping->setChecked(false);
|
||||
|
||||
}
|
||||
|
||||
void GroupByDialog::accept() {
|
||||
|
||||
emit Accepted(CollectionModel::Grouping(
|
||||
p_->mapping_.get<tag_index>().find(ui_->combobox_first->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->combobox_second->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->combobox_third->currentIndex())->group_by)
|
||||
p_->mapping_.get<tag_index>().find(ui_->combobox_third->currentIndex())->group_by),
|
||||
ui_->checkbox_separate_albums_by_grouping->isChecked()
|
||||
);
|
||||
QDialog::accept();
|
||||
|
||||
}
|
||||
|
||||
void GroupByDialog::CollectionGroupingChanged(const CollectionModel::Grouping g) {
|
||||
void GroupByDialog::CollectionGroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping) {
|
||||
|
||||
ui_->combobox_first->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[0])->combo_box_index);
|
||||
ui_->combobox_second->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[1])->combo_box_index);
|
||||
ui_->combobox_third->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[2])->combo_box_index);
|
||||
ui_->checkbox_separate_albums_by_grouping->setChecked(separate_albums_by_grouping);
|
||||
|
||||
}
|
||||
|
||||
@@ -45,11 +45,11 @@ class GroupByDialog : public QDialog {
|
||||
~GroupByDialog() override;
|
||||
|
||||
public slots:
|
||||
void CollectionGroupingChanged(const CollectionModel::Grouping g);
|
||||
void CollectionGroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
|
||||
void accept() override;
|
||||
|
||||
signals:
|
||||
void Accepted(CollectionModel::Grouping g);
|
||||
void Accepted(CollectionModel::Grouping g, bool separate_albums_by_grouping);
|
||||
|
||||
private slots:
|
||||
void Reset();
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>354</width>
|
||||
<height>246</height>
|
||||
<width>394</width>
|
||||
<height>273</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -370,6 +370,13 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkbox_separate_albums_by_grouping">
|
||||
<property name="text">
|
||||
<string>Separate albums by grouping tag</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2015, Nick Lanham <nick@afternight.org>
|
||||
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2019-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,35 +22,32 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QWidget>
|
||||
#include <QStandardItemModel>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QKeySequence>
|
||||
#include <QPushButton>
|
||||
#include <QTreeView>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "ui_savedgroupingmanager.h"
|
||||
|
||||
SavedGroupingManager::SavedGroupingManager(QWidget *parent)
|
||||
const char *SavedGroupingManager::kSavedGroupingsSettingsGroup = "SavedGroupings";
|
||||
|
||||
SavedGroupingManager::SavedGroupingManager(const QString &saved_groupings_settings_group, QWidget *parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_SavedGroupingManager),
|
||||
model_(new QStandardItemModel(0, 4, this)),
|
||||
filter_(nullptr) {
|
||||
saved_groupings_settings_group_(saved_groupings_settings_group) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
@@ -71,74 +68,84 @@ SavedGroupingManager::SavedGroupingManager(QWidget *parent)
|
||||
|
||||
SavedGroupingManager::~SavedGroupingManager() {
|
||||
delete ui_;
|
||||
delete model_;
|
||||
}
|
||||
|
||||
QString SavedGroupingManager::GetSavedGroupingsSettingsGroup(const QString &settings_group) {
|
||||
|
||||
if (settings_group.isEmpty() || settings_group == CollectionSettingsPage::kSettingsGroup) {
|
||||
return kSavedGroupingsSettingsGroup;
|
||||
}
|
||||
else {
|
||||
return QString(kSavedGroupingsSettingsGroup) + "_" + settings_group;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy g) {
|
||||
|
||||
switch (g) {
|
||||
case CollectionModel::GroupBy_None:
|
||||
case CollectionModel::GroupByCount: {
|
||||
case CollectionModel::GroupBy::None:
|
||||
case CollectionModel::GroupBy::GroupByCount: {
|
||||
return tr("None");
|
||||
}
|
||||
case CollectionModel::GroupBy_AlbumArtist: {
|
||||
case CollectionModel::GroupBy::AlbumArtist: {
|
||||
return tr("Album artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_Artist: {
|
||||
case CollectionModel::GroupBy::Artist: {
|
||||
return tr("Artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_Album: {
|
||||
case CollectionModel::GroupBy::Album: {
|
||||
return tr("Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_AlbumDisc: {
|
||||
case CollectionModel::GroupBy::AlbumDisc: {
|
||||
return tr("Album - Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_YearAlbum: {
|
||||
case CollectionModel::GroupBy::YearAlbum: {
|
||||
return tr("Year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_YearAlbumDisc: {
|
||||
case CollectionModel::GroupBy::YearAlbumDisc: {
|
||||
return tr("Year - Album - Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYearAlbum: {
|
||||
case CollectionModel::GroupBy::OriginalYearAlbum: {
|
||||
return tr("Original year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYearAlbumDisc: {
|
||||
case CollectionModel::GroupBy::OriginalYearAlbumDisc: {
|
||||
return tr("Original year - Album - Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_Disc: {
|
||||
case CollectionModel::GroupBy::Disc: {
|
||||
return tr("Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_Year: {
|
||||
case CollectionModel::GroupBy::Year: {
|
||||
return tr("Year");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYear: {
|
||||
case CollectionModel::GroupBy::OriginalYear: {
|
||||
return tr("Original year");
|
||||
}
|
||||
case CollectionModel::GroupBy_Genre: {
|
||||
case CollectionModel::GroupBy::Genre: {
|
||||
return tr("Genre");
|
||||
}
|
||||
case CollectionModel::GroupBy_Composer: {
|
||||
case CollectionModel::GroupBy::Composer: {
|
||||
return tr("Composer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Performer: {
|
||||
case CollectionModel::GroupBy::Performer: {
|
||||
return tr("Performer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Grouping: {
|
||||
case CollectionModel::GroupBy::Grouping: {
|
||||
return tr("Grouping");
|
||||
}
|
||||
case CollectionModel::GroupBy_FileType: {
|
||||
case CollectionModel::GroupBy::FileType: {
|
||||
return tr("File type");
|
||||
}
|
||||
case CollectionModel::GroupBy_Format: {
|
||||
case CollectionModel::GroupBy::Format: {
|
||||
return tr("Format");
|
||||
}
|
||||
case CollectionModel::GroupBy_Samplerate: {
|
||||
case CollectionModel::GroupBy::Samplerate: {
|
||||
return tr("Sample rate");
|
||||
}
|
||||
case CollectionModel::GroupBy_Bitdepth: {
|
||||
case CollectionModel::GroupBy::Bitdepth: {
|
||||
return tr("Bit depth");
|
||||
}
|
||||
case CollectionModel::GroupBy_Bitrate: {
|
||||
case CollectionModel::GroupBy::Bitrate: {
|
||||
return tr("Bitrate");
|
||||
}
|
||||
}
|
||||
@@ -151,7 +158,7 @@ void SavedGroupingManager::UpdateModel() {
|
||||
|
||||
model_->setRowCount(0); // don't use clear, it deletes headers
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
s.beginGroup(saved_groupings_settings_group_);
|
||||
int version = s.value("version").toInt();
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
@@ -186,7 +193,7 @@ void SavedGroupingManager::Remove() {
|
||||
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
s.beginGroup(saved_groupings_settings_group_);
|
||||
for (const QModelIndex &idx : ui_->list->selectionModel()->selectedRows()) {
|
||||
if (idx.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
|
||||
@@ -196,7 +203,8 @@ void SavedGroupingManager::Remove() {
|
||||
s.endGroup();
|
||||
}
|
||||
UpdateModel();
|
||||
filter_->UpdateGroupByActions();
|
||||
|
||||
emit UpdateGroupByActions();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2015, Nick Lanham <nick@afternight.org>
|
||||
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2019-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -40,14 +40,20 @@ class SavedGroupingManager : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SavedGroupingManager(QWidget *parent = nullptr);
|
||||
explicit SavedGroupingManager(const QString &saved_groupings_settings_group, QWidget *parent = nullptr);
|
||||
~SavedGroupingManager() override;
|
||||
|
||||
static const char *kSavedGroupingsSettingsGroup;
|
||||
|
||||
static QString GetSavedGroupingsSettingsGroup(const QString &settings_group);
|
||||
|
||||
void UpdateModel();
|
||||
void SetFilter(CollectionFilterWidget *filter) { filter_ = filter; }
|
||||
|
||||
static QString GroupByToString(const CollectionModel::GroupBy g);
|
||||
|
||||
signals:
|
||||
void UpdateGroupByActions();
|
||||
|
||||
private slots:
|
||||
void UpdateButtonState();
|
||||
void Remove();
|
||||
@@ -55,7 +61,7 @@ class SavedGroupingManager : public QDialog {
|
||||
private:
|
||||
Ui_SavedGroupingManager *ui_;
|
||||
QStandardItemModel *model_;
|
||||
CollectionFilterWidget *filter_;
|
||||
QString saved_groupings_settings_group_;
|
||||
};
|
||||
|
||||
#endif // SAVEDGROUPINGMANAGER_H
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#cmakedefine HAVE_MUSICBRAINZ
|
||||
#cmakedefine HAVE_GLOBALSHORTCUTS
|
||||
#cmakedefine HAVE_X11_GLOBALSHORTCUTS
|
||||
#cmakedefine HAVE_ICU
|
||||
|
||||
#cmakedefine USE_INSTALL_PREFIX
|
||||
|
||||
#cmakedefine HAVE_GSTREAMER
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
#include <QContextMenuEvent>
|
||||
#include <QPaintEvent>
|
||||
|
||||
#include "core/imageutils.h"
|
||||
#include "utilities/imageutils.h"
|
||||
#include "covermanager/albumcoverchoicecontroller.h"
|
||||
|
||||
#include "contextview.h"
|
||||
@@ -63,7 +63,7 @@ ContextAlbum::ContextAlbum(QWidget *parent)
|
||||
cover_loader_options_.desired_height_ = width();
|
||||
cover_loader_options_.pad_output_image_ = true;
|
||||
cover_loader_options_.scale_output_image_ = true;
|
||||
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
|
||||
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
|
||||
if (!image.isNull()) {
|
||||
pixmap_current_ = QPixmap::fromImage(image);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ void ContextAlbum::Init(ContextView *context_view, AlbumCoverChoiceController *a
|
||||
|
||||
QSize ContextAlbum::sizeHint() const {
|
||||
|
||||
return QSize(pixmap_current_.width(), pixmap_current_.height());
|
||||
return QSize(pixmap_current_.width() / devicePixelRatioF(), pixmap_current_.height() / devicePixelRatioF());
|
||||
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ void ContextAlbum::DrawImage(QPainter *p, const QPixmap &pixmap, const qreal opa
|
||||
if (qFuzzyCompare(opacity, static_cast<qreal>(0.0))) return;
|
||||
|
||||
p->setOpacity(opacity);
|
||||
p->drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap);
|
||||
p->drawPixmap(0, 0, pixmap.width() / pixmap.devicePixelRatioF(), pixmap.height() / pixmap.devicePixelRatioF(), pixmap);
|
||||
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ void ContextAlbum::FadePreviousCoverFinished(std::shared_ptr<PreviousCover> prev
|
||||
|
||||
void ContextAlbum::ScaleCover() {
|
||||
|
||||
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
|
||||
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
|
||||
if (image.isNull()) {
|
||||
pixmap_current_ = QPixmap();
|
||||
}
|
||||
@@ -248,7 +248,7 @@ void ContextAlbum::ScaleCover() {
|
||||
void ContextAlbum::ScalePreviousCovers() {
|
||||
|
||||
for (std::shared_ptr<PreviousCover> previous_cover : previous_covers_) {
|
||||
QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
|
||||
QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
|
||||
if (image.isNull()) {
|
||||
previous_cover->pixmap = QPixmap();
|
||||
}
|
||||
|
||||
@@ -51,8 +51,9 @@
|
||||
#include "core/application.h"
|
||||
#include "core/player.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "utilities/strutils.h"
|
||||
#include "utilities/timeutils.h"
|
||||
#include "widgets/resizabletextedit.h"
|
||||
#include "engine/engine_fwd.h"
|
||||
#include "engine/enginebase.h"
|
||||
@@ -337,11 +338,11 @@ void ContextView::ReloadSettings() {
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
title_fmt_ = s.value(ContextSettingsPage::kSettingsTitleFmt, "%title% - %artist%").toString();
|
||||
summary_fmt_ = s.value(ContextSettingsPage::kSettingsSummaryFmt, "%album%").toString();
|
||||
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUM], true).toBool());
|
||||
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], false).toBool());
|
||||
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], false).toBool());
|
||||
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS], true).toBool());
|
||||
action_search_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS], true).toBool());
|
||||
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], true).toBool());
|
||||
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], false).toBool());
|
||||
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE)], false).toBool());
|
||||
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], true).toBool());
|
||||
action_search_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS)], true).toBool());
|
||||
font_headline_ = s.value("font_headline", font().family()).toString();
|
||||
font_normal_ = s.value("font_normal", font().family()).toString();
|
||||
font_size_headline_ = s.value("font_size_headline", ContextSettingsPage::kDefaultFontSizeHeadline).toReal();
|
||||
@@ -406,7 +407,7 @@ void ContextView::SearchLyrics() {
|
||||
if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && action_search_lyrics_->isChecked() && !song_playing_.artist().isEmpty() && !song_playing_.title().isEmpty() && !lyrics_tried_ && lyrics_id_ == -1) {
|
||||
lyrics_fetcher_->Clear();
|
||||
lyrics_tried_ = true;
|
||||
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.album(), song_playing_.title()));
|
||||
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -434,8 +435,8 @@ void ContextView::NoSong() {
|
||||
widget_album_->show();
|
||||
}
|
||||
|
||||
textedit_top_->setStyleSheet("font: 20pt 'Open Sans', 'FreeSans', 'FreeSerif', 'Liberation Serif'; font-weight: Regular;");
|
||||
textedit_top_->setText(tr("No song playing"));
|
||||
textedit_top_->setFont(QFont(font_headline_, static_cast<int>(font_size_headline_ * 1.6)));
|
||||
textedit_top_->SetText(tr("No song playing"));
|
||||
|
||||
QString html;
|
||||
if (collectionview_->TotalSongs() == 1) html += tr("%1 song").arg(collectionview_->TotalSongs());
|
||||
@@ -450,27 +451,28 @@ void ContextView::NoSong() {
|
||||
else html += tr("%1 albums").arg(collectionview_->TotalAlbums());
|
||||
html += "<br />";
|
||||
|
||||
label_stop_summary_->setStyleSheet(QString("font: %1pt \"%2\"; font-weight: regular;").arg(font_size_normal_).arg(font_normal_));
|
||||
label_stop_summary_->setFont(QFont(font_normal_, static_cast<int>(font_size_normal_)));
|
||||
label_stop_summary_->setText(html);
|
||||
|
||||
}
|
||||
|
||||
void ContextView::UpdateFonts() {
|
||||
|
||||
QString font_style = QString("font: %2pt \"%1\"; font-weight: regular;").arg(font_normal_).arg(font_size_normal_);
|
||||
QFont font(font_normal_, static_cast<int>(font_size_normal_));
|
||||
font.setBold(false);
|
||||
for (QLabel *l : labels_play_all_) {
|
||||
l->setStyleSheet(font_style);
|
||||
l->setFont(font);
|
||||
}
|
||||
for (QTextEdit *e : textedit_play_) {
|
||||
e->setStyleSheet(font_style);
|
||||
e->setFont(font);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ContextView::SetSong() {
|
||||
|
||||
textedit_top_->setStyleSheet(QString("font: %2pt \"%1\"; font-weight: regular;").arg(font_headline_).arg(font_size_headline_));
|
||||
textedit_top_->setText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song_playing_, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song_playing_, "<br />", true)));
|
||||
textedit_top_->setFont(QFont(font_headline_, static_cast<int>(font_size_headline_)));
|
||||
textedit_top_->SetText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song_playing_, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song_playing_, "<br />", true)));
|
||||
|
||||
label_stop_summary_->clear();
|
||||
|
||||
@@ -542,9 +544,9 @@ void ContextView::SetSong() {
|
||||
|
||||
if (action_show_output_->isChecked()) {
|
||||
widget_play_output_->show();
|
||||
Engine::EngineType enginetype(Engine::None);
|
||||
Engine::EngineType enginetype(Engine::EngineType::None);
|
||||
if (app_->player()->engine()) enginetype = app_->player()->engine()->type();
|
||||
QIcon icon_engine = IconLoader::Load(EngineName(enginetype), 32);
|
||||
QIcon icon_engine = IconLoader::Load(EngineName(enginetype), true, 32);
|
||||
|
||||
label_engine_icon_->setPixmap(icon_engine.pixmap(QSize(32, 32)));
|
||||
label_engine_->setText(EngineDescription(enginetype));
|
||||
@@ -562,7 +564,7 @@ void ContextView::SetSong() {
|
||||
label_device_title_->show();
|
||||
label_device_icon_->show();
|
||||
label_device_->show();
|
||||
QIcon icon_device = IconLoader::Load(device.iconname, 32);
|
||||
QIcon icon_device = IconLoader::Load(device.iconname, true, 32);
|
||||
label_device_icon_->setPixmap(icon_device.pixmap(QSize(32, 32)));
|
||||
label_device_->setText(device.description);
|
||||
}
|
||||
@@ -584,7 +586,7 @@ void ContextView::SetSong() {
|
||||
}
|
||||
|
||||
if (action_show_lyrics_->isChecked() && !lyrics_.isEmpty()) {
|
||||
textedit_play_lyrics_->setText(lyrics_);
|
||||
textedit_play_lyrics_->SetText(lyrics_);
|
||||
textedit_play_lyrics_->show();
|
||||
}
|
||||
else {
|
||||
@@ -599,7 +601,7 @@ void ContextView::SetSong() {
|
||||
|
||||
void ContextView::UpdateSong(const Song &song) {
|
||||
|
||||
textedit_top_->setText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song, "<br />", true)));
|
||||
textedit_top_->SetText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song, "<br />", true)));
|
||||
|
||||
if (action_show_data_->isChecked()) {
|
||||
if (song.filetype() != song_playing_.filetype()) label_filetype_->setText(song.TextForFiletype());
|
||||
@@ -655,6 +657,8 @@ void ContextView::UpdateSong(const Song &song) {
|
||||
|
||||
song_playing_ = song;
|
||||
|
||||
widget_stacked_->updateGeometry();
|
||||
|
||||
}
|
||||
|
||||
void ContextView::ResetSong() {
|
||||
@@ -681,7 +685,7 @@ void ContextView::UpdateLyrics(const quint64 id, const QString &provider, const
|
||||
lyrics_id_ = -1;
|
||||
|
||||
if (action_show_lyrics_->isChecked() && !lyrics_.isEmpty()) {
|
||||
textedit_play_lyrics_->setText(lyrics_);
|
||||
textedit_play_lyrics_->SetText(lyrics_);
|
||||
textedit_play_lyrics_->show();
|
||||
}
|
||||
else {
|
||||
@@ -735,7 +739,7 @@ void ContextView::ActionShowAlbum() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUM], action_show_album_->isChecked());
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], action_show_album_->isChecked());
|
||||
s.endGroup();
|
||||
if (song_playing_.is_valid()) SetSong();
|
||||
|
||||
@@ -745,7 +749,7 @@ void ContextView::ActionShowData() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], action_show_data_->isChecked());
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], action_show_data_->isChecked());
|
||||
s.endGroup();
|
||||
if (song_playing_.is_valid()) SetSong();
|
||||
|
||||
@@ -755,7 +759,7 @@ void ContextView::ActionShowOutput() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], action_show_output_->isChecked());
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE)], action_show_output_->isChecked());
|
||||
s.endGroup();
|
||||
if (song_playing_.is_valid()) SetSong();
|
||||
|
||||
@@ -765,7 +769,7 @@ void ContextView::ActionShowLyrics() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS], action_show_lyrics_->isChecked());
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], action_show_lyrics_->isChecked());
|
||||
s.endGroup();
|
||||
|
||||
if (song_playing_.is_valid()) SetSong();
|
||||
@@ -778,7 +782,7 @@ void ContextView::ActionSearchLyrics() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(ContextSettingsPage::kSettingsGroup);
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS], action_search_lyrics_->isChecked());
|
||||
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS)], action_search_lyrics_->isChecked());
|
||||
s.endGroup();
|
||||
|
||||
if (song_playing_.is_valid()) SetSong();
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, Arnaud Bienner <arnaud.bienner@gmail.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QSettings>
|
||||
|
||||
#include "appearance.h"
|
||||
#include "settings/appearancesettingspage.h"
|
||||
|
||||
const QPalette Appearance::kDefaultPalette = QPalette();
|
||||
|
||||
Appearance::Appearance(QObject *parent) : QObject(parent) {
|
||||
|
||||
QPalette p = QApplication::palette();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
background_color_ = s.value(AppearanceSettingsPage::kBackgroundColor, p.color(QPalette::WindowText)).value<QColor>();
|
||||
foreground_color_ = s.value(AppearanceSettingsPage::kForegroundColor, p.color(QPalette::Window)).value<QColor>();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void Appearance::LoadUserTheme() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
bool use_a_custom_color_set = s.value(AppearanceSettingsPage::kUseCustomColorSet).toBool();
|
||||
s.endGroup();
|
||||
|
||||
if (use_a_custom_color_set) {
|
||||
ChangeForegroundColor(foreground_color_);
|
||||
ChangeBackgroundColor(background_color_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Appearance::ResetToSystemDefaultTheme() {
|
||||
QApplication::setPalette(kDefaultPalette);
|
||||
}
|
||||
|
||||
void Appearance::ChangeForegroundColor(const QColor &color) {
|
||||
|
||||
// Get the application palette
|
||||
QPalette p = QApplication::palette();
|
||||
|
||||
// Modify the palette
|
||||
p.setColor(QPalette::WindowText, color);
|
||||
p.setColor(QPalette::Text, color);
|
||||
|
||||
// Make the modified palette the new application's palette
|
||||
QApplication::setPalette(p);
|
||||
foreground_color_ = color;
|
||||
|
||||
}
|
||||
|
||||
void Appearance::ChangeBackgroundColor(const QColor &color) {
|
||||
|
||||
// Get the application palette
|
||||
QPalette p = QApplication::palette();
|
||||
|
||||
// Modify the palette
|
||||
p.setColor(QPalette::Window, color);
|
||||
p.setColor(QPalette::Base, color);
|
||||
|
||||
// Make the modified palette the new application's palette
|
||||
QApplication::setPalette(p);
|
||||
background_color_ = color;
|
||||
|
||||
}
|
||||
@@ -29,18 +29,15 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "core/lazy.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "database.h"
|
||||
#include "taskmanager.h"
|
||||
#include "player.h"
|
||||
#include "appearance.h"
|
||||
|
||||
#include "engine/devicefinders.h"
|
||||
#ifndef Q_OS_WIN
|
||||
@@ -66,6 +63,7 @@
|
||||
#include "lyrics/lololyricsprovider.h"
|
||||
#include "lyrics/musixmatchlyricsprovider.h"
|
||||
#include "lyrics/chartlyricsprovider.h"
|
||||
#include "lyrics/lyricscomlyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
#include "scrobbler/lastfmimport.h"
|
||||
@@ -111,7 +109,6 @@ class ApplicationImpl {
|
||||
QTimer::singleShot(30s, db, &Database::DoBackup);
|
||||
return db;
|
||||
}),
|
||||
appearance_([app]() { return new Appearance(app); }),
|
||||
task_manager_([app]() { return new TaskManager(app); }),
|
||||
player_([app]() { return new Player(app, app); }),
|
||||
device_finders_([app]() { return new DeviceFinders(app); }),
|
||||
@@ -158,6 +155,7 @@ class ApplicationImpl {
|
||||
lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network(), app));
|
||||
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network(), app));
|
||||
lyrics_providers->AddProvider(new ChartLyricsProvider(lyrics_providers->network(), app));
|
||||
lyrics_providers->AddProvider(new LyricsComLyricsProvider(lyrics_providers->network(), app));
|
||||
lyrics_providers->ReloadSettings();
|
||||
return lyrics_providers;
|
||||
}),
|
||||
@@ -185,7 +183,6 @@ class ApplicationImpl {
|
||||
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
Lazy<Database> database_;
|
||||
Lazy<Appearance> appearance_;
|
||||
Lazy<TaskManager> task_manager_;
|
||||
Lazy<Player> player_;
|
||||
Lazy<DeviceFinders> device_finders_;
|
||||
@@ -317,7 +314,6 @@ void Application::ReloadSettings() { emit SettingsChanged(); }
|
||||
void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) { emit SettingsDialogRequested(page); }
|
||||
|
||||
TagReaderClient *Application::tag_reader_client() const { return p_->tag_reader_client_.get(); }
|
||||
Appearance *Application::appearance() const { return p_->appearance_.get(); }
|
||||
Database *Application::database() const { return p_->database_.get(); }
|
||||
TaskManager *Application::task_manager() const { return p_->task_manager_.get(); }
|
||||
Player *Application::player() const { return p_->player_.get(); }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user