Compare commits

...

No commits in common. "master" and "qt6" have entirely different histories.
master ... qt6

174 changed files with 19680 additions and 24789 deletions

258
.clang-format Normal file
View file

@ -0,0 +1,258 @@
---
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -1
AlignAfterOpenBracket: AlwaysBreak
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: false
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterExternBlock: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAfterAttributes: Never
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
ColumnLimit: 160
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: "^<.*"
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: ".*"
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: "([-_](test|unittest))?$"
IncludeIsMainSourceRegex: ""
IndentAccessModifiers: true
IndentCaseBlocks: false
IndentCaseLabels: true
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: true
IndentWidth: 2
IndentWrappedFunctionNames: false
InsertBraces: false
InsertNewlineAtEOF: true
InsertTrailingCommas: None
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
LineEnding: LF
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: All
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PackConstructorInitializers: NextLine
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
PPIndentWidth: -1
QualifierAlignment: Leave
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- "c++"
- "C++"
CanonicalDelimiter: ""
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: pb
BasedOnStyle: google
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RemoveSemicolon: false
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: LexicographicNumeric
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAroundPointerQualifiers: Default
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
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseTab: Never
WhitespaceSensitiveMacros:
- BOOST_PP_STRINGIZE
- CF_SWIFT_NAME
- NS_SWIFT_NAME
- PP_STRINGIZE
- STRINGIZE
---

2
.clangd Normal file
View file

@ -0,0 +1,2 @@
CompileFlags:
Add: [-std=c++20]

12
.github/FUNDING.yml vendored
View file

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: JoshStrobl
patreon: joshuastrobl
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: joshuastrobl
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

94
.gitignore vendored
View file

@ -1,4 +1,90 @@
DesiredSettings.md
builddir
.buildconfig
src/theme/style.css
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
CMakeLists.txt.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe
.cache/
.ccls-cache/
.idea/
.kdev4/
.qt/
.rcc/
.zed/
bin/
build*/
cmake-build-debug/
**/qmldir
**/meta_types
**/qmltypes
**/*.qrc
CMakeCache.txt
*.kdev4

View file

@ -1,17 +0,0 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/usr/include/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu17",
"cppStandard": "c++20",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}

50
.vscode/launch.json vendored
View file

@ -1,50 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch (GDB)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/builddir/src/com.github.joshstrobl.koto",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{"name" : "G_MESSAGES_DEBUG", "value": "all" },
{ "name": "GTK_THEME", "value": "Adwaita:dark" }
],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "Meson Configure and Build"
},
{
"name": "Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/builddir/src/com.github.joshstrobl.koto",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{"name" : "G_MESSAGES_DEBUG", "value": "all" },
{ "name": "GTK_THEME", "value": "Adwaita:dark" }
],
"externalConsole": false,
"linux": {
"MIMode": "gdb",
"miDebuggerPath": ""
},
"preLaunchTask": "Meson Configure and Build"
},
]
}

22
.vscode/settings.json vendored
View file

@ -1,22 +0,0 @@
{
"files.associations": {
"glib.h": "c",
"ios": "c",
"__node_handle": "c",
"gtk.h": "c",
"gtktreeview.h": "c",
"cartographer.h": "c",
"structs.h": "c",
"gst.h": "c",
"player.h": "c",
"config.h": "c",
"toml.h": "c",
"chrono": "c",
"sqlite3.h": "c",
"unistd.h": "c",
"ui.h": "c",
"koto-utils.h": "c",
"random": "c",
"add-remove-track-popover.h": "c"
}
}

92
.vscode/tasks.json vendored
View file

@ -1,92 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Clean builddir",
"type": "shell",
"command": "rm",
"args": [
"-rf",
"builddir"
],
"problemMatcher": []
},
{
"label": "Format",
"type": "shell",
"command": "uncrustify",
"args": [
"-c",
"jsc.cfg",
"--no-backup",
"**/*.c",
"**/*.h"
]
},
{
"label": "Meson Configure and Build",
"type": "shell",
"command": "",
"dependsOrder": "sequence",
"dependsOn": ["Meson Configure", "Meson Compile"]
},
{
"label": "Meson Configure",
"type": "shell",
"command": "meson",
"args": [
"--prefix=/usr",
"--libdir=\"libdir\"",
"--sysconfdir=/etc",
"builddir"
],
"problemMatcher": []
},
{
"label": "Meson Compile",
"type": "shell",
"command": "meson",
"args": [
"compile",
"-C",
"builddir",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Meson Dist",
"type": "shell",
"command": "meson",
"args": [
"dist",
"-C",
"builddir",
"--formats",
"xztar",
"--include-subprojects"
],
"problemMatcher": []
},
{
"label": "Meson Install",
"type": "shell",
"command": "sudo",
"args": [
"meson",
"install",
"-C",
"builddir",
"--destdir",
"/",
"--no-rebuild"
],
"problemMatcher": []
}
]
}

8
CMakeLists.txt Normal file
View file

@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.16)
project(koto VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 23)
add_subdirectory(desktop)

509
COPYING
View file

@ -1,202 +1,373 @@
Mozilla Public License Version 2.0
==================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
1. Definitions
--------------
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1. Definitions.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
1.3. "Contribution"
means Covered Software of a particular Contributor.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
1.5. "Incompatible With Secondary Licenses"
means
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
1.8. "License"
means this document.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
1.10. "Modifications"
means any of the following:
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
(b) any new file in Source Code Form that contains any Covered
Software.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
2. License Grants and Conditions
--------------------------------
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
2.1. Grants
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
2.2. Effective Date
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
END OF TERMS AND CONDITIONS
2.3. Limitations on Grant Scope
APPENDIX: How to apply the Apache License to your work.
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
(a) for any code that a Contributor has removed from Covered Software;
or
Copyright [yyyy] [name of copyright owner]
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
http://www.apache.org/licenses/LICENSE-2.0
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -1,26 +1,13 @@
# Koto
# Koto Qt
![koto alpha](https://raw.githubusercontent.com/JoshStrobl/koto/master/.github/koto-alpha-image.png)
Work in progress implementation of Koto in Qt6, Kirigami, and C++.
Koto is an in-development audiobook, music, and podcast manager that is designed *for* and caters *to* a modern desktop
Linux experience. **Nothing to see here yet**.
To clone this repository on [Radicle](https://radicle.xyz), simple run:
## Blog
```
rad clone rad://z2RuqdobvbGje3mWhtMQ9cUVNfizp
```
- [Dev Diary 13: Koto - The Resurrection](https://joshuastrobl.com/2024/11/01/dev-diary-13-koto-the-resurrection)
- [Dev Diary 12: Koto August Progress Report](https://joshuastrobl.com/2021/09/06/dev-diary-12-koto-august-progress-report)
- [Dev Diary 11: Koto July Progress Report](https://joshuastrobl.com/2021/08/08/dev-diary-11-koto-july-progress-report/)
- [Dev Diary 10: Koto June Progress Report](https://joshuastrobl.com/2021/07/08/dev-diary-10-koto-june-progress-report/)
- [Dev Diary 9: Koto May Progress Report (B-side)](https://joshuastrobl.com/2021/06/10/dev-diary-9-koto-may-progress-report-b-side/)
- [Dev Diary 8: Koto May Progress Report (A-side)](https://joshuastrobl.com/2021/05/27/dev-diary-8-koto-may-progress-report-a-side/)
- [Dev Diary 7: Koto April Progress Report (B-side)](https://joshuastrobl.com/2021/05/07/dev-diary-7-koto-april-progress-report-b-side/)
- [Dev Diary 6: Koto April Progress Report (A-side)](https://joshuastrobl.com/2021/04/26/dev-diary-6-koto-april-progress-report-a-side/)
- [Dev Diary 5: Koto March Progress Report (B-side)](https://joshuastrobl.com/2021/04/08/dev-diary-5-koto-march-progress-report-b-side/)
- [Dev Diary 4: Koto March Progress Report (A-side)](https://joshuastrobl.com/2021/03/26/dev-diary-4-koto-march-progress-report-a-side/)
- [Dev Diary 3: Koto February Progress Report (B-side)](https://joshuastrobl.com/2021/03/05/dev-diary-3-koto-february-progress-report-b-side/)
- [Dev Diary 2: Koto February Progress Report (A-side)](https://joshuastrobl.com/2021/02/17/dev-diary-2-koto-february-progress-report-a-side/)
- [Dev Diary 1: Koto - Foundations](https://joshuastrobl.com/2021/01/25/dev-diary-1-koto-foundations/)
## LICENSE
## License
Koto is licensed under the Apache 2.0 license.
Licensed under the Mozilla Public License 2.0 (MPL-2.0).

17
Taskfile.yml Normal file
View file

@ -0,0 +1,17 @@
version: "3"
tasks:
sdbus-gen-desktop:
cmds:
- sdbus-c++-xml2cpp desktop/dbus/schema.xml --adaptor=desktop/dbus/daemon-server.h --proxy=desktop/dbus/daemon-client.h
setup-desktop:
desc: "Run cmake configuration for desktop Koto"
cmds:
- cmake -S . -B build
build: cmake --build build
build-watch-desktop: watchman-make -p '**/*.cpp' '**/*.h' --run "task cook-desktop"
cook-desktop:
cmds:
- task setup-desktop
- task build
install: sudo make install -C build

View file

@ -1,14 +0,0 @@
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
# Package managers set this so we don't need to run
if not destdir:
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])

View file

@ -1,40 +0,0 @@
{
"app-id" : "com.github.joshstrobl.koto",
"runtime" : "org.gnome.Platform",
"runtime-version" : "40",
"sdk" : "org.gnome.Sdk",
"command" : "com.github.joshstrobl.koto",
"finish-args" : [
"--share=network",
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland"
],
"cleanup" : [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"modules" : [
{
"name" : "koto",
"builddir" : true,
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : "https://github.com/JoshStrobl/koto.git"
}
]
}
],
"build-options" : {
"env" : { }
}
}

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.github.joshstrobl.koto.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>
<name>Koto</name>
<summary>Koto is an in-development audiobook, music, and podcast manager.</summary>
<description>
<p>Koto is an in-development audiobook, music, and podcast manager that is designed for and caters to a modern desktop Linux experience.</p>
</description>
<launchable type="desktop-id">com.github.joshstrobl.koto.desktop</launchable>
<url type="homepage">https://github.com/JoshStrobl/koto</url>
<url type="bugtracker">https://github.com/JoshStrobl/koto/issues</url>
<url type="donation">https://patreon.com/joshuastrobl</url>
<url type="donation">https://liberapay.com/joshuastrobl</url>
<developer_name>Joshua Strobl</developer_name>
<update_contact>joshua.strobl_AT_outlook.com</update_contact>
<content_rating type="oars-1.0">
<content_attribute id="language-humor">mild</content_attribute>
</content_rating>
<provides>
<binary>com.github.joshstrobl.koto</binary>
</provides>
<recommends>
<control>pointing</control>
<control>touch</control>
<display_length compare="ge">1600</display_length>
</recommends>
<requires>
<control>keyboard</control>
<display_length compare="ge">1366</display_length>
</requires>
</component>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="koto">
<schema id="com.github.joshstrobl.koto" path="/com/github/joshstrobl/koto/">
</schema>
</schemalist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -1,41 +0,0 @@
desktop_file = i18n.merge_file(
input: 'com.github.joshstrobl.koto.desktop.in',
output: 'com.github.joshstrobl.koto.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'applications')
)
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
appstream_file = i18n.merge_file(
input: 'com.github.joshstrobl.koto.appdata.xml.in',
output: 'com.github.joshstrobl.koto.appdata.xml',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'appdata')
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate appstream file (relaxed)', appstream_util,
args: ['validate-relax', appstream_file]
)
endif
install_data('com.github.joshstrobl.koto.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif

View file

@ -1,181 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="520"
height="240"
version="1.1"
viewBox="0 0 137.58 63.5"
id="svg49"
sodipodi:docname="business-and-personal-finance.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview51"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.5480769"
inkscape:cx="308.75881"
inkscape:cy="167.13279"
inkscape:window-width="3840"
inkscape:window-height="2087"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg49" />
<defs
id="defs15">
<linearGradient
id="linearGradient295062"
x1="-134.86"
x2="-134.86"
y1="-123.4"
y2="40.627"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#5f38ad"
offset="0"
id="stop7" />
<stop
stop-color="#874ff5"
offset="1"
id="stop9" />
</linearGradient>
<clipPath
id="clipPath294187-9">
<rect
x="-218.13"
y="-82.027"
width="39.22"
height="24.317"
rx="5.2917"
ry="5.2917"
fill="#e8cc4c"
fill-rule="evenodd"
id="rect12" />
</clipPath>
</defs>
<g
id="g1680"
transform="scale(1.0000242,1)"
style="stroke-width:0.999988">
<g
id="g1664"
style="stroke-width:0.999988">
<rect
transform="translate(0,-11.25)"
x="2.7931001e-06"
y="11.25"
width="137.58"
height="63.5"
fill="#ffffff"
stroke-opacity="0.82206"
stroke-width="1.05829"
style="paint-order:stroke fill markers"
id="rect17" />
<g
transform="matrix(0.14144,0,0,0.14144,136.46,21.232)"
stroke-width="7.06981"
id="g39">
<rect
x="-267.14999"
y="-123.41"
width="264.57999"
height="164.03999"
fill="url(#linearGradient295062)"
fill-rule="evenodd"
id="rect19"
style="fill:url(#linearGradient295062);stroke-width:7.06973" />
<g
transform="translate(-22.74,-0.10437)"
stroke-width="7.06981"
id="g25">
<rect
x="-218.13"
y="-82.027"
width="39.220001"
height="24.316999"
rx="5.2916999"
ry="5.2916999"
fill="#e8cc4c"
fill-rule="evenodd"
id="rect21"
style="stroke-width:7.06973" />
<path
d="m -267.18,-101.62 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m 7.9375,-63.5 v 63.5 m -134.94,-63.5 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58 m -137.58,7.9375 h 137.58"
clip-path="url(#clipPath294187-9)"
fill="none"
stroke="#665c0c"
stroke-width="3.74115px"
id="path23" />
</g>
<g
fill="#ffffff"
fill-rule="evenodd"
id="g37"
style="stroke-width:7.06973">
<rect
x="-240.87"
y="8.8177996"
width="151.45"
height="15.875"
rx="5.2916999"
ry="5.2916999"
id="rect27"
style="stroke-width:7.06981" />
<rect
x="-240.87"
y="-35.048"
width="40.146999"
height="15.875"
rx="5.2916999"
ry="5.2916999"
id="rect29"
style="stroke-width:7.06981" />
<rect
x="-187.03999"
y="-35.048"
width="40.146999"
height="15.875"
rx="5.2916999"
ry="5.2916999"
id="rect31"
style="stroke-width:7.06981" />
<rect
x="-133.21001"
y="-35.048"
width="40.146999"
height="15.875"
rx="5.2916999"
ry="5.2916999"
id="rect33"
style="stroke-width:7.06981" />
<rect
x="-79.375"
y="-35.048"
width="40.146999"
height="15.875"
rx="5.2916999"
ry="5.2916999"
id="rect35"
style="stroke-width:7.06981" />
</g>
</g>
<rect
x="2.7931001e-06"
y="1.1538e-07"
width="137.58"
height="63.5"
fill="#ffffff"
fill-opacity="0.46872"
style="stroke-width:0.999976;paint-order:stroke fill markers"
id="rect41" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="520"
height="240"
version="1.1"
viewBox="0 0 137.58 63.5"
id="svg65"
sodipodi:docname="foreign-languages.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview67"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="2.2583333"
inkscape:cx="136.16236"
inkscape:cy="120"
inkscape:window-width="3840"
inkscape:window-height="2087"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg65" />
<defs
id="defs29">
<clipPath
id="clipPath301434">
<rect
y="-3.9777e-7"
width="137.58"
height="63.5"
fill="#a0a0ff"
fill-opacity=".66627"
fill-rule="evenodd"
id="rect26" />
</clipPath>
</defs>
<g
id="g1521"
transform="scale(1.0000242,1)"
style="stroke-width:0.999988"
inkscape:export-filename="/home/joshua/Code/Personal/Koto/data/genres/foreign-language.png"
inkscape:export-xdpi="96.0084"
inkscape:export-ydpi="96.0084">
<rect
y="0"
width="137.58"
height="63.5"
fill="#a0a0ff"
fill-rule="evenodd"
id="rect31"
x="0"
style="stroke-width:0.999976" />
<g
clip-path="url(#clipPath301434)"
id="g53"
style="stroke-width:0.999988">
<g
id="g43"
style="stroke-width:0.999988">
<path
d="m 141.34,38.614 h -38.032 c -1.9369,0 -3.5071,1.5702 -3.5071,3.5071 v 18.824 c 0,1.9369 1.5702,3.5066 3.5071,3.5066 h 22.416 l 6.1064,11.291 v -11.291 h 9.5088 c 1.9369,0 3.5071,-1.5697 3.5071,-3.5066 v -18.824 c 0,-1.9369 -1.5702,-3.5071 -3.5071,-3.5071"
fill="#f99746"
id="path33"
style="stroke-width:0.999976" />
<path
d="m 101.58,0.73638 h 34.001 c 1.7314,0 3.1353,1.6392 3.1353,2.9966 v 11.868 c 0,2.0377 -1.4039,3.2468 -3.1353,3.2468 h -20.04 l -5.4593,7.9149 v -7.9149 h -8.5011 c -1.7314,0 -3.1353,-1.1001 -3.1353,-2.4582 v -12.051 c 0,-2.2834 1.4038,-3.6022 3.1353,-3.6022"
fill="#c0d8fb"
id="path35"
style="stroke-width:0.999976" />
<path
d="M 37.569,1.5124 H 7.089 c -1.5522,0 -2.8106,1.2578 -2.8106,2.8104 v 15.087 c 0,1.5518 1.2584,2.8104 2.8106,2.8104 h 17.965 l 4.8943,9.0492 v -9.0492 h 7.6204 c 1.5526,0 2.8104,-1.2586 2.8104,-2.8104 V 4.3228 c 0,-1.5526 -1.2578,-2.8104 -2.8104,-2.8104"
fill="#ebf3fa"
id="path37"
style="stroke-width:0.999976" />
<path
d="m 21.158,14.314 c -0.20383,-1.0087 -0.82632,-2.5543 -1.4703,-3.7031 l 0.63324,-0.22511 c 0.6761,1.1487 1.3093,2.6618 1.5239,3.6706 z m -3.8958,-3.6172 c -0.03218,0.0758 -0.10737,0.11835 -0.23609,0.11835 -0.34339,1.3205 -0.9337,2.7261 -1.6099,3.5847 -0.13947,-0.11835 -0.39707,-0.27926 -0.56888,-0.36512 0.65467,-0.81534 1.2129,-2.2 1.5455,-3.5739 z m 3.4344,-0.35352 c 0.08578,-0.39761 0.19316,-0.99868 0.28978,-1.5781 h -2.1358 v 6.2674 c 0,0.7728 -0.27903,0.91281 -1.7815,0.91281 -0.04293,-0.21505 -0.1503,-0.52602 -0.26835,-0.7403 0.26835,0.01002 0.52595,0.01002 0.72978,0.01002 h 0.42941 c 0.13939,0 0.18241,-0.04254 0.18241,-0.18256 v -6.2674 h -1.728 c -0.32188,0.78362 -0.69753,1.4922 -1.1054,2.0283 -0.12881,-0.12841 -0.37564,-0.34346 -0.54745,-0.44016 0.77272,-0.9979 1.406,-2.6827 1.7709,-4.3676 l 0.89084,0.23594 c -0.02143,0.075032 -0.09655,0.11835 -0.23617,0.10753 -0.13947,0.59023 -0.31121,1.1913 -0.51512,1.7599 h 4.7438 l 0.37557,0.010777 c 0,0.18256 -0.2576,1.5881 -0.48301,2.3609 z m -7.3517,-0.2576 c -0.35422,0.62195 -0.72986,1.1913 -1.1163,1.674 -0.09662,-0.15007 -0.32188,-0.42933 -0.46151,-0.5794 1.0089,-1.2021 1.9748,-3.2412 2.5866,-5.227 l 0.89084,0.26843 c -0.04301,0.10753 -0.13964,0.12841 -0.2576,0.11835 -0.28986,0.85866 -0.63324,1.7274 -1.0196,2.5435 l 0.31128,0.085872 c -0.02158,0.064217 -0.0752,0.11758 -0.21474,0.12841 v 6.826 h -0.71896 v -5.8381"
fill="#2b478b"
opacity="0.54345"
id="path39"
style="stroke-width:0.999976" />
<path
d="m 33.617,10.751 v 0.69776 h -2.586 v 3.5522 c 0,0.82617 -0.32258,0.95535 -2.2217,0.93369 -0.04332,-0.19339 -0.1609,-0.50437 -0.26843,-0.71942 0.5152,0.01078 0.98707,0.02166 1.288,0.02166 0.36512,0 0.47187,0 0.47187,-0.22511 v -3.563 h -2.6294 v -0.69776 h 2.6294 v -1.8248 l 0.35429,0.021655 c 0.55852,-0.47265 1.1487,-1.148 1.5881,-1.749 h -4.1316 v -0.67688 h 4.6793 l 0.13924,-0.042537 0.56934,0.35429 c -0.03248,0.053389 -0.10753,0.085872 -0.17173,0.10753 -0.55852,0.79368 -1.4814,1.8132 -2.2967,2.4785 v 1.3313 z M 25.4284,8.9053 c -0.22534,0.9979 -0.47218,2.0066 -0.6976,2.887 0.45068,0.24677 0.91227,0.54691 1.3629,0.84783 0.42964,-0.99868 0.74062,-2.2433 0.89069,-3.7348 z m 1.932,-0.70859 0.40767,0.10753 -0.06422,0.15007 c -0.15084,1.8991 -0.49353,3.4238 -1.0304,4.6151 0.52602,0.40767 0.97624,0.80451 1.2663,1.1696 l -0.44016,0.59023 c -0.26842,-0.34346 -0.67609,-0.73025 -1.148,-1.1163 -0.54761,0.9979 -1.2666,1.7281 -2.1253,2.2433 -0.09662,-0.18256 -0.30053,-0.44016 -0.45068,-0.59023 0.8049,-0.42933 1.481,-1.1271 2.0176,-2.0933 -0.39707,-0.30014 -0.81565,-0.5794 -1.2235,-0.82617 l -0.12879,0.47188 -0.61174,-0.32181 c 0.26828,-0.94452 0.60099,-2.3076 0.91227,-3.6915 h -1.0733 V 8.22824 h 1.2235 c 0.17173,-0.80451 0.32188,-1.5773 0.45068,-2.2642 l 0.88009,0.10753 c -0.01078,0.075032 -0.07503,0.11758 -0.21459,0.12841 -0.11804,0.61189 -0.26827,1.3096 -0.42925,2.0283 h 1.6525 l 0.12919,-0.031709"
fill="#2b478b"
opacity="0.54345"
id="path41"
style="stroke-width:0.999976" />
</g>
<text
x="107.10843"
y="11.536377"
fill="#333333"
font-family="sans-serif"
font-size="6.7091px"
letter-spacing="0px"
opacity="0.53934"
stroke-width="0.264577px"
word-spacing="0px"
style="line-height:4.19317px"
xml:space="preserve"
id="text47"><tspan
x="107.10843"
y="11.536377"
fill="#333333"
font-family="'Noto Sans'"
font-size="6.7091px"
font-stretch="condensed"
font-weight="bold"
stroke-width="0.264577px"
id="tspan45">Bonjour</tspan></text>
<text
x="111.31922"
y="55.259583"
fill="#5a5a5a"
font-family="sans-serif"
font-size="9.9404px"
letter-spacing="0px"
opacity="0.48424"
stroke-width="0.264577px"
word-spacing="0px"
style="line-height:6.21273px"
xml:space="preserve"
id="text51"><tspan
x="111.31922"
y="55.259583"
fill="#5a5a5a"
font-family="'Noto Sans'"
font-size="9.9404px"
font-stretch="condensed"
font-weight="bold"
stroke-width="0.264577px"
id="tspan49">Hello</tspan></text>
</g>
<rect
y="0"
width="137.58"
height="63.5"
fill="#a0a0ff"
fill-opacity="0.66627"
fill-rule="evenodd"
id="rect55"
x="0"
style="stroke-width:0.999976" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(0 -11.25)">
<g transform="translate(-312.3 -265.79)" opacity=".996">
<path d="m312.49 277.23-0.18709 2.4322 2.4322-0.1871-0.74834-0.74833a2.1167 2.1167 0 0 1 2.9934-1e-5 2.1167 2.1167 0 0 1 0 2.9934 2.1167 2.1167 0 0 1-2.9934 2e-5l-0.74836 0.74835a3.175 3.175 0 0 0 4.4901-3e-5 3.175 3.175 0 0 0 2e-5 -4.4901 3.175 3.175 0 0 0-4.4902-2e-5z" fill="#666" stroke-width=".26458"/>
<path d="m315.46 279.29v1.1199s0.43968 0.4382 0.54774 0.54626" fill="none" stroke="#666" stroke-linecap="round" stroke-width=".79375"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 695 B

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(0 -11.25)">
<g transform="matrix(.99913 0 0 1 -320.37 -265.83)" opacity=".996">
<path d="m326.82 277.26 0.18709 2.4322-2.4322-0.18709 0.74834-0.74833a2.1167 2.1167 0 0 0-2.9934-1e-5 2.1167 2.1167 0 0 0 0 2.9934 2.1167 2.1167 0 0 0 2.9934 1e-5l0.74836 0.74835a3.175 3.175 0 0 1-4.4901-3e-5 3.175 3.175 0 0 1-2e-5 -4.4901 3.175 3.175 0 0 1 4.4902-1e-5z" fill="#666" opacity=".998" stroke-width=".2647"/>
<path d="m323.85 279.32v1.12s-0.43968 0.4382-0.54774 0.54625" fill="none" opacity=".998" stroke="#666" stroke-linecap="round" stroke-width=".7941"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 733 B

View file

@ -1,572 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="520"
height="240"
version="1.1"
viewBox="0 0 137.58 63.5"
id="svg296"
sodipodi:docname="mystery-and-thriller.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview298"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="2.2583333"
inkscape:cx="136.16236"
inkscape:cy="120"
inkscape:window-width="3840"
inkscape:window-height="2087"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg296" />
<defs
id="defs72">
<clipPath
id="clipPath302672">
<rect
x="15.037"
y="6.529"
width="114.27"
height="52.739"
fill="#333"
fill-rule="evenodd"
opacity=".56765"
id="rect69" />
</clipPath>
</defs>
<g
id="g1377"
transform="scale(1.000023,0.99999772)"
style="stroke-width:0.999989"
inkscape:export-filename="/home/joshua/Code/Personal/Koto/data/genres/mystery-and-thriller.png"
inkscape:export-xdpi="96.0084"
inkscape:export-ydpi="96.0084">
<rect
x="0.0054583"
y="0.065999985"
width="137.58"
height="63.5"
fill="#333333"
fill-rule="evenodd"
id="rect74"
style="stroke-width:0.999978" />
<g
transform="matrix(1.204,0,0,1.204,-18.1,-7.7951)"
clip-path="url(#clipPath302672)"
stroke-width="0.830531"
id="g284">
<g
stroke-width="0.830531"
id="g266">
<path
d="m 130.09,41.568 c 0.14098,0.10843 2.3979,1.8472 2.5041,2.1724 0.10831,0.32528 -0.8044,3.3497 -1.3399,4.2255 -0.0152,0.02595 -0.0325,0.05201 -0.0476,0.07379 -1.7019,2.6623 -4.438,4.8715 -7.8071,6.3197 1.6563,-1.8731 2.5127,-4.9604 2.5127,-4.9604 l -1.0428,-3.6921 2.387,-0.55303 0.49429,-2.6926 c -0.53762,-1.4808 -1.7778,-2.5084 -1.7778,-2.5084 -0.26226,-0.35552 -0.55499,-0.70895 -0.86939,-1.0341 0,0 6.0706,2.4368 7.9914,3.6401 h 0.002 c -0.0108,0.15615 -0.0282,0.30791 -0.0476,0.46174 l -2.9594,-1.4525"
fill="#2c2c2c"
id="path76"
style="stroke-width:0.830522" />
<path
d="m 130.09,41.568 2.9594,1.4525 c -0.21246,1.7584 -0.83476,3.4257 -1.7952,4.9454 0.53554,-0.87588 1.4482,-3.9003 1.3399,-4.2255 -0.10623,-0.32517 -2.3632,-2.0639 -2.5041,-2.1724"
fill="#878787"
id="path78"
style="stroke-width:0.830522" />
<path
d="m 128.21,21.038 c 1.4786,0.45097 3.2932,1.7345 3.2196,3.1805 0,0 -0.35992,-0.73281 -1.3161,-1.7345 -0.89534,-0.93878 -1.7951,-1.3918 -1.9035,-1.446"
fill="#1e1e1e"
id="path80"
style="stroke-width:0.830522" />
<path
d="m 131.43,24.219 c 0.0737,-1.446 -1.741,-2.7296 -3.2196,-3.1805 -0.004,-0.0021 -0.007,-0.0021 -0.0109,-0.0043 0,0 0.004,0.0022 0.0109,0.0043 0.10842,0.05421 1.0082,0.50727 1.9035,1.446 0.95615,1.0017 1.3161,1.7345 1.3161,1.7345 z m -2.0163,2.8184 c -0.16044,0.07588 -5.3876,2.5193 -15.395,2.5193 v -4.2298 c 3.3345,0 6.4196,-0.48781 8.3427,-1.2857 1.8385,-0.76305 2.7209,-1.4655 2.736,-1.7344 0.0152,-0.27107 -1.2314,-0.48352 -1.2314,-0.48352 l -0.0196,0.0022 c -0.0672,-0.53333 -0.15824,-1.1078 -0.25798,-1.704 -0.013,-0.07159 -0.0259,-0.14527 -0.039,-0.21906 l 0.28185,-0.0065 c 8.219,0.54202 9.0712,4.1302 7.367,6.2375 -1.006,1.2445 -2.6992,2.4888 -6.3437,3.2997 -0.0888,0.01946 -0.1799,0.03904 -0.27096,0.0585 -0.50089,0.10634 -1.0363,0.20388 -1.6109,0.29273 -2.3003,0.35552 -5.2206,0.56588 -8.9541,0.56588 v -0.38158 c 9.0993,0 13.108,-1.4049 15.395,-2.9312"
fill="#2c2c2c"
id="path82"
style="stroke-width:0.830522" />
<path
d="m 126.82,44.722 h 0.002 c 0.38586,-1.3984 0.60052,-2.3502 0.60052,-2.3502 0,0 -0.5616,-1.2076 -1.4591,-2.4196 0,0 1.2402,1.0276 1.7778,2.5084 l -0.4943,2.6926 -2.387,0.55303 1.0428,3.6921 c 0,0 -0.85641,3.0873 -2.5127,4.9604 -0.27964,0.12141 -0.56368,0.2363 -0.85202,0.34472 1.1577,-1.3072 2.1874,-3.6185 2.9983,-5.8883 -0.0152,-0.03695 -1.3246,-3.2955 -1.3246,-3.2955 l 0.76745,-0.23423 1.8407,-0.56357"
fill="#1e1e1e"
id="path84"
style="stroke-width:0.830522" />
<path
d="m 125.1,48.82 c -0.0411,0.02815 -0.68508,0.43788 -2.1875,-0.34254 -1.5935,-0.82826 -1.5935,-4.49 -1.5935,-4.49 0.0305,3.1805 -0.78911,8.8651 -1.8428,11.642 -0.0607,0.01516 -0.1236,0.02815 -0.18639,0.04114 0.54423,-2.3284 1.4027,-9.1577 1.6326,-14.329 0.078,-1.728 0.0846,-3.2695 -0.0152,-4.3968 0.45306,-0.63087 0.77394,-1.1707 0.86059,-1.496 1.2272,1.6737 3.3345,3.4689 3.3345,3.4689 0.31439,0.32517 0.60712,0.6786 0.86939,1.0341 0.89753,1.2119 1.4591,2.4196 1.4591,2.4196 0,0 -0.21465,0.95175 -0.60052,2.3502 h -0.002 l -1.8407,0.56357 -2.9745,-4.2991 c 0.0824,0.14526 1.4113,2.4822 2.207,4.5333 0,0 1.3095,3.2585 1.3246,3.2955 -0.81089,2.2698 -1.8406,4.5811 -2.9983,5.8883 l -0.002,0.0021 c -0.17561,0.06733 -0.35563,0.13244 -0.53553,0.1952 0.9865,-1.3896 2.1528,-3.3908 3.0916,-6.0813"
fill="#2c2c2c"
id="path86"
style="stroke-width:0.830522" />
<g
fill="#1e1e1e"
id="g94"
style="stroke-width:0.830522">
<path
d="m 114.02,29.557 c 10.008,0 15.235,-2.4434 15.395,-2.5193 -2.2873,1.5263 -6.296,2.9312 -15.395,2.9312 v -0.41193"
id="path88"
style="stroke-width:0.830531" />
<path
d="m 123.87,21.823 c 0,0 1.2466,0.21245 1.2314,0.48352 -0.0152,0.26887 -0.89753,0.97133 -2.736,1.7344 -1.9231,0.79791 -5.0082,1.2857 -8.3427,1.2857 v -0.40544 c 7.625,0 9.9123,-2.3307 9.9123,-2.3307 -0.0217,-0.24489 -0.0498,-0.50078 -0.0846,-0.76525 l 0.0196,-0.0022"
id="path90"
style="stroke-width:0.830531" />
<path
d="m 122.01,40.987 2.9745,4.2991 -0.76745,0.23423 c -0.79571,-2.0511 -2.1246,-4.3881 -2.207,-4.5333"
id="path92"
style="stroke-width:0.830531" />
</g>
<path
d="m 124.83,29.633 c -0.16693,1.1642 -0.71764,1.9599 -2.0358,2.244 0,0 1.0082,-0.78054 1.2423,-1.0776 0.54643,-0.69377 0.54643,-1.3074 0.54643,-1.3074 0.0911,-0.01946 0.1821,-0.03904 0.27095,-0.0585 -0.006,0.06719 -0.0152,0.13449 -0.0239,0.19948"
fill="#878787"
id="path96"
style="stroke-width:0.830522" />
<path
d="m 122.91,48.477 c 1.5025,0.78042 2.1464,0.37069 2.1875,0.34254 -0.93877,2.6905 -2.1051,4.6917 -3.0916,6.0813 -0.81309,0.28626 -1.6607,0.53125 -2.5323,0.72854 1.0537,-2.7773 1.8733,-8.4619 1.8428,-11.642 0,0 0,3.6617 1.5935,4.49"
fill="#2c2c2c"
id="path98"
style="stroke-width:0.830522" />
<path
d="m 124.59,29.492 c 0,0 0,0.61361 -0.54643,1.3074 -0.23411,0.29702 -1.2423,1.0776 -1.2423,1.0776 -0.0824,0.01726 -0.16692,0.03464 -0.25577,0.04761 0.15823,-0.75876 0.29481,-1.4374 0.36849,-1.8189 0.0412,-0.20388 0.0651,-0.32088 0.0651,-0.32088 0.57457,-0.08885 1.11,-0.18639 1.6109,-0.29273"
fill="#1e1e1e"
id="path100"
style="stroke-width:0.830522" />
<path
d="m 115.13,21.015 c 4.774,-0.16264 7.5469,-1.342 8.3167,-1.7193 0.0237,0.13229 0.0476,0.26226 0.0715,0.39235 -0.85201,0.51827 -2.0987,0.89117 -3.6032,1.1838 0,0 1.5848,0.34254 3.6791,-0.75007 0.0997,0.59624 0.19079,1.1707 0.25798,1.704 0.0348,0.26446 0.0629,0.52036 0.0846,0.76525 0,0 -2.2873,2.3307 -9.9123,2.3307 v -3.8873 c 0.3795,0 0.74799,-0.0065 1.1057,-0.01946"
fill="#4d4d4d"
id="path102"
style="stroke-width:0.830522" />
<path
d="m 123.55,19.902 c 0.0131,0.07379 0.0261,0.14747 0.039,0.21906 -2.0943,1.0926 -3.6791,0.75007 -3.6791,0.75007 1.5046,-0.29262 2.7512,-0.66551 3.6032,-1.1838 0.013,0.07159 0.0239,0.14318 0.0368,0.21465"
fill="#2c2c2c"
id="path104"
style="stroke-width:0.830522" />
<path
d="m 123.35,18.777 c 0.0325,0.17341 0.0651,0.34683 0.0955,0.51816 -0.76977,0.3773 -3.5427,1.5567 -8.3167,1.7193 5.0733,-0.51827 6.9551,-1.548 8.2212,-2.2375"
fill="#1e1e1e"
id="path106"
style="stroke-width:0.830522" />
<path
d="m 122.3,12.542 c 0.15604,1.7301 0.63516,4.0499 1.0428,6.2353 -1.2661,0.68949 -3.148,1.7192 -8.2212,2.2375 -0.35772,0.01297 -0.72621,0.01946 -1.1057,0.01946 v -8.5594 c 3.432,0 3.677,-2.0185 3.677,-2.0185 -0.71973,0.51388 -1.4786,0.96484 -3.677,0.96484 v -0.26887 c 0.45538,0.02386 1.1405,0.0065 1.9275,-0.22554 0.89973,-0.26226 1.548,-0.68926 1.9294,-0.98639 0.63967,-0.023872 1.4375,0.0044 2.9897,1.1209 0.68301,0.49221 1.1405,1.0472 1.4375,1.4808 z m -2.1139,-1.2791 c 0.0629,0.0563 1.0602,0.94955 1.2987,1.8818 0.245,0.96473 0.9192,4.284 0.9192,4.284 l -0.40325,-4.529 c 0,0 -0.33814,-0.85201 -1.8146,-1.6368"
fill="#2c2c2c"
id="path108"
style="stroke-width:0.830522" />
<path
d="m 122.97,29.784 c 0,0 -0.0239,0.117 -0.0651,0.32088 0,0 -0.53114,1.3138 -0.92789,2.5669 -0.4943,0.22554 -0.99728,0.24292 -2.4,0.26887 -1.8169,0.03255 -2.3328,0.39235 -3.0504,0.41413 -0.71764,0.02155 -1.5025,-0.83047 -2.5106,-0.83047 v -2.1745 c 3.7334,0 6.6538,-0.21037 8.9541,-0.56588"
fill="#1e1e1e"
id="path110"
style="stroke-width:0.830522" />
<path
d="m 122.91,30.105 c -0.0737,0.38158 -0.21026,1.0602 -0.36849,1.8189 -0.0326,0.16056 -0.0672,0.32517 -0.10195,0.49001 -0.16692,0.10843 -0.30999,0.19299 -0.45745,0.25798 0.39675,-1.2532 0.92789,-2.5669 0.92789,-2.5669"
fill="#878787"
id="path112"
style="stroke-width:0.830522" />
<path
d="m 122.01,12.9 0.40325,4.529 c 0,0 -0.6742,-3.3193 -0.9192,-4.284 -0.23852,-0.93229 -1.2358,-1.8255 -1.2987,-1.8818 1.4765,0.78482 1.8146,1.6368 1.8146,1.6368"
fill="#4d4d4d"
id="path114"
style="stroke-width:0.830522" />
<path
d="m 121.98,32.672 c 0.14746,-0.06499 0.29053,-0.14955 0.45745,-0.25798 -0.24928,1.1946 -0.52035,2.4629 -0.67431,3.0353 -0.0866,0.32528 -0.40753,0.8651 -0.86058,1.496 -0.30791,0.42919 -0.67651,0.89974 -1.0732,1.3745 -0.74799,0.89117 -1.5979,1.7974 -2.3459,2.4607 0.12361,-0.12349 1.2769,-1.2834 2.2764,-2.851 1.0581,-1.6563 1.5935,-2.5322 1.8624,-3.9241 0.0737,-0.38587 0.20597,-0.8541 0.35772,-1.3333"
fill="#fbfbfb"
id="path116"
style="stroke-width:0.830522" />
<path
d="m 116.18,34.357 c 0.69389,0.17341 1.5134,1.3268 1.6283,2.2375 0.013,0.16913 0.013,0.27536 0.013,0.27536 0.004,-0.08897 0,-0.18002 -0.013,-0.27536 -0.0217,-0.33826 -0.0803,-0.92569 -0.24071,-1.3853 -0.20377,-0.57886 -1.11,-0.79792 -1.3876,-0.85213 z m -0.63296,7.5643 c -0.60492,0.14306 -1.2336,0.16693 -1.5242,0.16693 v -3.1415 c 0.81089,0 1.7756,-0.58755 1.8363,-0.62438 -0.0454,0.0044 -0.60481,0.07159 -1.8363,0.07159 v -0.88236 c 1.6305,0 3.1848,-0.22334 3.1848,-0.22334 -0.83684,-0.01517 -2.168,-0.35992 -2.3782,-0.34254 -0.20817,0.01726 -0.80661,0.41622 -0.80661,0.41622 v -1.7474 c 0.6808,0 0.98871,-0.27107 1.1275,-0.52256 0.8541,-0.1821 0.94736,-0.75019 0.94736,-0.75019 -0.14735,0.20168 -0.33595,0.34903 -0.83464,0.32957 0,0 -0.44877,0.62879 -1.2402,0.62879 v -2.7752 c 1.0082,0 1.793,0.85201 2.5106,0.83047 0.71764,-0.02178 1.2336,-0.38158 3.0504,-0.41413 1.4027,-0.02595 1.9057,-0.04333 2.4,-0.26887 -0.15175,0.47924 -0.28405,0.94747 -0.35772,1.3333 -0.26887,1.3919 -0.80429,2.2678 -1.8624,3.9241 -0.99948,1.5676 -2.1528,2.7275 -2.2764,2.851 -0.006,0.0044 -0.0109,0.0088 -0.0109,0.0088 -0.4812,0.42699 -0.917,0.75227 -1.2553,0.90831 -0.20596,0.09545 -0.43788,0.16913 -0.67419,0.22334"
fill="#878787"
id="path118"
style="stroke-width:0.830522" />
<path
d="m 120.9,36.945 c 0.0997,1.1273 0.0931,2.6688 0.0152,4.3968 -0.0846,0.56148 -0.19079,1.097 -0.30362,1.5869 -0.37498,1.6305 -0.82375,2.7774 -0.82375,2.7774 -0.84344,-1.173 -2.0489,-2.2028 -2.9616,-2.8922 -0.34034,-0.25589 -0.63955,-0.46614 -0.8629,-0.6157 1.6738,-0.52685 2.6189,-1.8364 3.8635,-3.8787 0.39664,-0.47472 0.76525,-0.94526 1.0732,-1.3745"
fill="#fbfbfb"
id="path120"
style="stroke-width:0.830522" />
<path
d="m 120.62,42.929 c 0.11283,-0.48989 0.21906,-1.0254 0.30362,-1.5869 -0.22983,5.1708 -1.0883,12 -1.6326,14.329 -0.34903,0.07597 -0.70246,0.14525 -1.0581,0.20593 1.1123,-3.0938 2.3502,-12.648 2.387,-12.948"
fill="#878787"
id="path122"
style="stroke-width:0.830522" />
<path
d="m 119.79,45.706 c 0,0 0.44877,-1.1469 0.82374,-2.7774 -0.0368,0.29922 -1.2747,9.8538 -2.387,12.948 -0.36849,0.0629 -0.7393,0.11933 -1.1165,0.16485 0,0 -0.49001,-4.9084 -0.64605,-6.7621 -0.15835,-1.8537 -1.0407,-4.2321 -1.0407,-4.2321 l 0.81089,-1.524 0.22983,-0.27315 c 0,0 2.0401,1.2531 3.3258,2.4564"
fill="#fbfbfb"
id="path124"
style="stroke-width:0.830522" />
<path
d="m 117.48,40.78 c 0.74799,-0.66331 1.5979,-1.5695 2.3459,-2.4607 -1.2446,2.0423 -2.1898,3.3519 -3.8635,3.8787 -0.24709,-0.16704 -0.40104,-0.26458 -0.42271,-0.27756 0.23632,-0.05421 0.46823,-0.12789 0.6742,-0.22334 0.33825,-0.15604 0.77405,-0.48132 1.2552,-0.90831 0,0 0.004,-0.0044 0.0109,-0.0088"
fill="#2c2c2c"
id="path126"
style="stroke-width:0.830522" />
<path
d="m 117.56,35.209 c 0.16043,0.45966 0.21905,1.0471 0.24071,1.3853 -0.11491,-0.91063 -0.93437,-2.0641 -1.6283,-2.2375 0.27755,0.05421 1.1838,0.27327 1.3876,0.85213"
fill="#2c2c2c"
id="path128"
style="stroke-width:0.830522" />
<g
fill="#1e1e1e"
id="g136"
style="stroke-width:0.830522">
<path
d="m 119.79,45.706 c -1.2857,-1.2034 -3.3258,-2.4564 -3.3258,-2.4564 l 0.36421,-0.4358 c 0.91271,0.68937 2.1182,1.7192 2.9616,2.8922"
id="path130"
style="stroke-width:0.830531" />
<path
d="m 117.7,10.456 c 0,0 -0.24501,2.0185 -3.677,2.0185 v -1.0537 c 2.1984,0 2.9573,-0.45097 3.677,-0.96484"
id="path132"
style="stroke-width:0.830531" />
<path
d="m 117.11,56.041 c -0.19079,0.02388 -0.38158,0.04542 -0.57457,0.06498 0,0 -0.24489,-5.2576 -0.54411,-7.3496 -0.29922,-2.0943 -0.9865,-3.768 -0.9865,-3.768 l 1.2293,-1.4656 -0.81089,1.524 c 0,0 0.88237,2.3785 1.0407,4.2321 0.15604,1.8537 0.64605,6.7621 0.64605,6.7621"
id="path134"
style="stroke-width:0.830531" />
</g>
<g
fill="#2c2c2c"
id="g144"
style="stroke-width:0.830522">
<path
d="m 116.83,42.814 -0.36421,0.4358 -0.22983,0.27315 -1.2293,1.4656 h -0.98651 v -2.528 c 0.75459,0 1.392,-0.08885 1.947,-0.26226 0.22334,0.14955 0.52256,0.3598 0.8629,0.6157"
id="path138"
style="stroke-width:0.830531" />
<path
d="m 116.54,56.106 c -0.14086,0.0152 -0.28184,0.02815 -0.42271,0.03696 l -0.002,-0.03696 c 0.12141,-2.1768 -0.34034,-6.9616 -0.34034,-6.9616 0,0 -0.0477,5.8733 -0.87379,7.0721 -0.29041,0.01085 -0.58314,0.01516 -0.87807,0.01516 v -11.243 h 0.9865 c 0,0 0.68728,1.6737 0.9865,3.768 0.29922,2.0921 0.54411,7.3496 0.54411,7.3496"
id="path140"
style="stroke-width:0.830531" />
<path
d="m 117.21,37.288 c 0,0 -1.5544,0.22334 -3.1848,0.22334 v -0.14967 c 0,0 0.59844,-0.39896 0.80661,-0.41622 0.21025,-0.01738 1.5414,0.32737 2.3782,0.34254"
id="path142"
style="stroke-width:0.830531" />
</g>
<path
d="m 115.77,49.145 c 0,0 0.46174,4.7848 0.34034,6.9616 l 0.002,0.03696 c -0.40104,0.03453 -0.80649,0.0606 -1.2163,0.07358 0.82606,-1.1989 0.87379,-7.0721 0.87379,-7.0721"
fill="#878787"
id="path146"
style="stroke-width:0.830522" />
<path
d="m 116.1,34.341 c 0,0 -0.0933,0.56809 -0.94736,0.75019 0.11921,-0.21906 0.11272,-0.42062 0.11272,-0.42062 0.49869,0.01946 0.68729,-0.12789 0.83464,-0.32957"
fill="#1e1e1e"
id="path148"
style="stroke-width:0.830522" />
<path
d="m 115.54,41.921 c 0.0217,0.01297 0.17562,0.11051 0.42271,0.27756 -0.555,0.17341 -1.1924,0.26226 -1.947,0.26226 v -0.37289 c 0.29065,0 0.91932,-0.02386 1.5242,-0.16693"
fill="#1e1e1e"
id="path150"
style="stroke-width:0.830522" />
<path
d="m 114.02,38.394 c 1.2315,0 1.7909,-0.06719 1.8363,-0.07159 -0.0607,0.03684 -1.0254,0.62438 -1.8363,0.62438 v -0.5528"
fill="#878787"
id="path152"
style="stroke-width:0.830522" />
<path
d="m 115.15,35.092 c -0.13877,0.25149 -0.44668,0.52256 -1.1275,0.52256 v -0.31439 c 0.79143,0 1.2402,-0.62879 1.2402,-0.62879 0,0 0.007,0.20156 -0.11272,0.42062"
fill="#1e1e1e"
id="path154"
style="stroke-width:0.830522" />
<path
d="m 97.954,41.568 c -0.14086,0.10843 -2.3978,1.8472 -2.504,2.1724 -0.10838,0.32528 0.80437,3.3497 1.3398,4.2255 0.01517,0.02595 0.03254,0.05201 0.04773,0.07379 1.7019,2.6623 4.438,4.8715 7.8071,6.3197 -1.6564,-1.8731 -2.5128,-4.9604 -2.5128,-4.9604 l 1.0429,-3.6921 -2.387,-0.55303 -0.49429,-2.6926 c 0.53762,-1.4808 1.7777,-2.5084 1.7777,-2.5084 0.26239,-0.35552 0.555,-0.70895 0.86939,-1.0341 0,0 -6.0705,2.4368 -7.9914,3.6401 h -0.002 c 0.01087,0.15615 0.02814,0.30791 0.04769,0.46174 l 2.9593,-1.4525"
fill="#2c2c2c"
id="path156"
style="stroke-width:0.830522" />
<path
d="m 97.954,41.568 -2.9593,1.4525 c 0.2125,1.7584 0.83469,3.4257 1.7951,4.9454 -0.53548,-0.87588 -1.4482,-3.9003 -1.3398,-4.2255 0.10625,-0.32517 2.3632,-2.0639 2.504,-2.1724"
fill="#878787"
id="path158"
style="stroke-width:0.830522" />
<path
d="m 99.828,21.038 c -1.4786,0.45097 -3.2933,1.7345 -3.2196,3.1805 0,0 0.35992,-0.73281 1.316,-1.7345 0.89545,-0.93878 1.7952,-1.3918 1.9036,-1.446"
fill="#1e1e1e"
id="path160"
style="stroke-width:0.830522" />
<path
d="m 96.608,24.219 c -0.07372,-1.446 1.741,-2.7296 3.2196,-3.1805 0.0043,-0.0021 0.0065,-0.0021 0.0109,-0.0043 0,0 -0.0044,0.0022 -0.0109,0.0043 -0.10843,0.05421 -1.0082,0.50727 -1.9036,1.446 -0.95604,1.0017 -1.316,1.7345 -1.316,1.7345 z m 7.8419,-4.0975 c 0.013,-0.07159 0.0259,-0.14527 0.0389,-0.21906 l -0.28184,-0.0065 c -8.219,0.54202 -9.071,4.1302 -7.3669,6.2375 1.006,1.2445 2.6992,2.4888 6.3436,3.2997 0.089,0.01946 0.18002,0.03904 0.27107,0.0585 0.50078,0.10634 1.0363,0.20388 1.6109,0.29273 2.3003,0.35552 5.2206,0.56588 8.954,0.56588 v -0.38158 c -9.0992,0 -13.108,-1.4049 -15.395,-2.9312 0.16032,0.07588 5.3875,2.5193 15.395,2.5193 v -4.2298 c -3.3345,0 -6.4196,-0.48781 -8.3427,-1.2857 -1.8384,-0.76305 -2.7209,-1.4655 -2.7361,-1.7344 -0.0152,-0.27107 1.2315,-0.48352 1.2315,-0.48352 l 0.0195,0.0022 c 0.0672,-0.53333 0.15823,-1.1078 0.25809,-1.704"
fill="#2c2c2c"
id="path162"
style="stroke-width:0.830522" />
<path
d="m 101.22,44.722 h -0.002 c -0.38587,-1.3984 -0.60052,-2.3502 -0.60052,-2.3502 0,0 0.56148,-1.2076 1.459,-2.4196 0,0 -1.2401,1.0276 -1.7777,2.5084 l 0.49429,2.6926 2.387,0.55303 -1.0429,3.6921 c 0,0 0.85641,3.0873 2.5128,4.9604 0.27964,0.12141 0.56368,0.2363 0.85201,0.34472 -1.1577,-1.3072 -2.1876,-3.6185 -2.9984,-5.8883 0.0152,-0.03695 1.3246,-3.2955 1.3246,-3.2955 l -0.76745,-0.23423 -1.8406,-0.56357"
fill="#1e1e1e"
id="path164"
style="stroke-width:0.830522" />
<path
d="m 106.03,40.987 c -0.0824,0.14526 -1.4114,2.4822 -2.2071,4.5333 0,0 -1.3095,3.2585 -1.3246,3.2955 0.81089,2.2698 1.8407,4.5811 2.9984,5.8883 l 0.002,0.0021 c 0.17561,0.06733 0.35551,0.13244 0.53542,0.1952 -0.98639,-1.3896 -2.1528,-3.3908 -3.0916,-6.0813 0.0412,0.02815 0.68509,0.43788 2.1876,-0.34254 1.5935,-0.82826 1.5935,-4.49 1.5935,-4.49 -0.0303,3.1805 0.78911,8.8651 1.8428,11.642 0.0607,0.01516 0.1236,0.02815 0.1865,0.04113 -0.54422,-2.3284 -1.4027,-9.1577 -1.6326,-14.329 -0.0781,-1.728 -0.0846,-3.2695 0.0152,-4.3968 -0.45317,-0.63087 -0.77405,-1.1707 -0.8607,-1.496 -1.2271,1.6737 -3.3345,3.4689 -3.3345,3.4689 -0.3144,0.32517 -0.60701,0.6786 -0.86939,1.0341 -0.89754,1.2119 -1.459,2.4196 -1.459,2.4196 0,0 0.21465,0.95175 0.60052,2.3502 h 0.002 l 1.8406,0.56357 2.9746,-4.2991"
fill="#2c2c2c"
id="path166"
style="stroke-width:0.830522" />
<g
fill="#1e1e1e"
id="g174"
style="stroke-width:0.830522">
<path
d="m 114.02,29.557 c -10.008,0 -15.235,-2.4434 -15.395,-2.5193 2.2873,1.5263 6.296,2.9312 15.395,2.9312 v -0.41193"
id="path168"
style="stroke-width:0.830531" />
<path
d="m 104.17,21.823 c 0,0 -1.2467,0.21245 -1.2315,0.48352 0.0152,0.26887 0.89766,0.97133 2.7361,1.7344 1.9231,0.79791 5.0082,1.2857 8.3427,1.2857 v -0.40544 c -7.625,0 -9.9123,-2.3307 -9.9123,-2.3307 0.0218,-0.24489 0.0499,-0.50078 0.0846,-0.76525 l -0.0195,-0.0022"
id="path170"
style="stroke-width:0.830531" />
<path
d="m 106.03,40.987 -2.9746,4.2991 0.76745,0.23423 c 0.79571,-2.0511 2.1248,-4.3881 2.2071,-4.5333"
id="path172"
style="stroke-width:0.830531" />
</g>
<path
d="m 103.21,29.633 c 0.16681,1.1642 0.71752,1.9599 2.0357,2.244 0,0 -1.008,-0.78054 -1.2423,-1.0776 -0.5463,-0.69377 -0.5463,-1.3074 -0.5463,-1.3074 -0.091,-0.01946 -0.1821,-0.03904 -0.27107,-0.0585 0.007,0.06719 0.0152,0.13449 0.024,0.19948"
fill="#878787"
id="path176"
style="stroke-width:0.830522" />
<path
d="m 105.13,48.477 c -1.5025,0.78042 -2.1463,0.37069 -2.1876,0.34254 0.93879,2.6905 2.1052,4.6917 3.0916,6.0813 0.81309,0.28626 1.6608,0.53125 2.5323,0.72854 -1.0537,-2.7773 -1.8732,-8.4619 -1.8428,-11.642 0,0 0,3.6617 -1.5935,4.49"
fill="#2c2c2c"
id="path178"
style="stroke-width:0.830522" />
<path
d="m 103.45,29.492 c 0,0 0,0.61361 0.5463,1.3074 0.23424,0.29702 1.2423,1.0776 1.2423,1.0776 0.0825,0.01726 0.16693,0.03464 0.25589,0.04761 -0.15824,-0.75876 -0.29493,-1.4374 -0.36861,-1.8189 -0.0411,-0.20388 -0.065,-0.32088 -0.065,-0.32088 -0.57458,-0.08885 -1.1101,-0.18639 -1.6109,-0.29273"
fill="#1e1e1e"
id="path180"
style="stroke-width:0.830522" />
<path
d="m 112.91,21.015 c -4.774,-0.16264 -7.5469,-1.342 -8.3166,-1.7193 -0.0239,0.13229 -0.0477,0.26226 -0.0716,0.39235 0.85213,0.51827 2.0987,0.89117 3.6034,1.1838 0,0 -1.5848,0.34254 -3.6791,-0.75007 -0.0999,0.59624 -0.19091,1.1707 -0.25809,1.704 -0.0346,0.26446 -0.0628,0.52036 -0.0846,0.76525 0,0 2.2873,2.3307 9.9123,2.3307 v -3.8873 c -0.37938,0 -0.74799,-0.0065 -1.1057,-0.01946"
fill="#4d4d4d"
id="path182"
style="stroke-width:0.830522" />
<path
d="m 104.49,19.902 c -0.013,0.07379 -0.0259,0.14747 -0.0389,0.21906 2.0943,1.0926 3.6791,0.75007 3.6791,0.75007 -1.5047,-0.29262 -2.7512,-0.66551 -3.6034,-1.1838 -0.013,0.07159 -0.0239,0.14318 -0.0368,0.21465"
fill="#2c2c2c"
id="path184"
style="stroke-width:0.830522" />
<path
d="m 104.69,18.777 c -0.0324,0.17341 -0.065,0.34683 -0.0953,0.51816 0.76965,0.3773 3.5425,1.5567 8.3166,1.7193 -5.0732,-0.51827 -6.955,-1.548 -8.2212,-2.2375"
fill="#1e1e1e"
id="path186"
style="stroke-width:0.830522" />
<path
d="m 105.74,12.542 c -0.15616,1.7301 -0.63528,4.0499 -1.0429,6.2353 1.2663,0.68949 3.1481,1.7192 8.2212,2.2375 0.35772,0.01297 0.72633,0.01946 1.1057,0.01946 v -8.5594 c -3.432,0 -3.677,-2.0185 -3.677,-2.0185 0.71984,0.51388 1.4786,0.96484 3.677,0.96484 v -0.26887 c -0.45526,0.02386 -1.1404,0.0065 -1.9274,-0.22554 -0.89974,-0.26226 -1.548,-0.68926 -1.9296,-0.98639 -0.63956,-0.023872 -1.4375,0.0044 -2.9898,1.1209 -0.68288,0.49221 -1.1403,1.0472 -1.4374,1.4808 z m 2.1138,-1.2791 c -0.0628,0.0563 -1.0602,0.94955 -1.2986,1.8818 -0.245,0.96473 -0.91931,4.284 -0.91931,4.284 l 0.40324,-4.529 c 0,0 0.33826,-0.85201 1.8146,-1.6368"
fill="#2c2c2c"
id="path188"
style="stroke-width:0.830522" />
<path
d="m 105.07,29.784 c 0,0 0.0239,0.117 0.065,0.32088 0,0 0.53125,1.3138 0.92789,2.5669 0.4943,0.22554 0.9974,0.24292 2.4001,0.26887 1.8167,0.03255 2.3327,0.39235 3.0504,0.41413 0.71751,0.02155 1.5023,-0.83047 2.5105,-0.83047 v -2.1745 c -3.7333,0 -6.6537,-0.21037 -8.954,-0.56588"
fill="#1e1e1e"
id="path190"
style="stroke-width:0.830522" />
<path
d="m 105.13,30.105 c 0.0737,0.38158 0.21037,1.0602 0.36861,1.8189 0.0325,0.16056 0.0672,0.32517 0.10194,0.49001 0.16693,0.10843 0.30999,0.19299 0.45734,0.25798 -0.39664,-1.2532 -0.92789,-2.5669 -0.92789,-2.5669"
fill="#878787"
id="path192"
style="stroke-width:0.830522" />
<path
d="m 106.03,12.9 -0.40324,4.529 c 0,0 0.67431,-3.3193 0.91931,-4.284 0.2384,-0.93229 1.2358,-1.8255 1.2986,-1.8818 -1.4764,0.78482 -1.8146,1.6368 -1.8146,1.6368"
fill="#4d4d4d"
id="path194"
style="stroke-width:0.830522" />
<path
d="m 106.06,32.672 c -0.14735,-0.06499 -0.29041,-0.14955 -0.45734,-0.25798 0.24918,1.1946 0.52024,2.4629 0.6742,3.0353 0.0866,0.32528 0.40753,0.8651 0.8607,1.496 0.3079,0.42919 0.67639,0.89974 1.0732,1.3745 0.74799,0.89117 1.5979,1.7974 2.3458,2.4607 -0.12349,-0.12349 -1.2769,-1.2834 -2.2764,-2.851 -1.058,-1.6563 -1.5935,-2.5322 -1.8624,-3.9241 -0.0737,-0.38587 -0.20597,-0.8541 -0.35772,-1.3333"
fill="#fbfbfb"
id="path196"
style="stroke-width:0.830522" />
<path
d="m 112.5,41.921 c 0.60481,0.14306 1.2336,0.16693 1.5241,0.16693 v -3.1415 c -0.81089,0 -1.7756,-0.58755 -1.8363,-0.62438 0.0455,0.0044 0.60492,0.07159 1.8363,0.07159 v -0.88236 c -1.6304,0 -3.1848,-0.22334 -3.1848,-0.22334 0.83684,-0.01517 2.168,-0.35992 2.3783,-0.34254 0.20805,0.01726 0.80649,0.41622 0.80649,0.41622 v -1.7474 c -0.68068,0 -0.98859,-0.27107 -1.1274,-0.52256 -0.85421,-0.1821 -0.94746,-0.75019 -0.94746,-0.75019 0.14746,0.20168 0.33605,0.34903 0.83475,0.32957 0,0 0.44876,0.62879 1.2401,0.62879 v -2.7752 c -1.0082,0 -1.793,0.85201 -2.5105,0.83047 -0.71776,-0.02178 -1.2337,-0.38158 -3.0504,-0.41413 -1.4027,-0.02595 -1.9058,-0.04333 -2.4001,-0.26887 0.15175,0.47924 0.28404,0.94747 0.35772,1.3333 0.26887,1.3919 0.8044,2.2678 1.8624,3.9241 0.99948,1.5676 2.1529,2.7275 2.2764,2.851 0.007,0.0044 0.0109,0.0088 0.0109,0.0088 0.48132,0.42699 0.91711,0.75227 1.2552,0.90831 0.20597,0.09545 0.438,0.16913 0.67432,0.22334 z m -0.63307,-7.5643 c -0.69377,0.17341 -1.5134,1.3268 -1.6282,2.2375 -0.0131,0.16913 -0.0131,0.27536 -0.0131,0.27536 -0.004,-0.08897 0,-0.18002 0.0131,-0.27536 0.0217,-0.33826 0.0802,-0.92569 0.2406,-1.3853 0.20377,-0.57886 1.11,-0.79792 1.3876,-0.85213"
fill="#878787"
id="path198"
style="stroke-width:0.830522" />
<path
d="m 107.14,36.945 c -0.0997,1.1273 -0.0933,2.6688 -0.0152,4.3968 0.0846,0.56148 0.19079,1.097 0.3035,1.5869 0.3751,1.6305 0.82387,2.7774 0.82387,2.7774 0.84332,-1.173 2.0488,-2.2028 2.9616,-2.8922 0.34035,-0.25589 0.63956,-0.46614 0.86279,-0.6157 -1.6737,-0.52685 -2.6189,-1.8364 -3.8634,-3.8787 -0.39676,-0.47472 -0.76525,-0.94526 -1.0732,-1.3745"
fill="#fbfbfb"
id="path200"
style="stroke-width:0.830522" />
<path
d="m 107.42,42.929 c -0.11272,-0.48989 -0.21894,-1.0254 -0.30351,-1.5869 0.22983,5.1708 1.0883,12 1.6326,14.329 0.34903,0.07597 0.70247,0.14525 1.058,0.20593 -1.1122,-3.0938 -2.3502,-12.648 -2.387,-12.948"
fill="#878787"
id="path202"
style="stroke-width:0.830522" />
<path
d="m 108.25,45.706 c 0,0 -0.44877,-1.1469 -0.82386,-2.7774 0.0368,0.29922 1.2748,9.8538 2.387,12.948 0.36849,0.0629 0.7393,0.11933 1.1166,0.16485 0,0 0.48989,-4.9084 0.64604,-6.7621 0.15824,-1.8537 1.0406,-4.2321 1.0406,-4.2321 l -0.81077,-1.524 -0.22983,-0.27315 c 0,0 -2.0402,1.2531 -3.3258,2.4564"
fill="#fbfbfb"
id="path204"
style="stroke-width:0.830522" />
<path
d="m 110.55,40.78 c -0.74787,-0.66331 -1.5978,-1.5695 -2.3458,-2.4607 1.2445,2.0423 2.1898,3.3519 3.8634,3.8787 0.24721,-0.16704 0.40116,-0.26458 0.42282,-0.27756 -0.23631,-0.05421 -0.46834,-0.12789 -0.67431,-0.22334 -0.33814,-0.15604 -0.77394,-0.48132 -1.2553,-0.90831 0,0 -0.004,-0.0044 -0.0109,-0.0088"
fill="#2c2c2c"
id="path206"
style="stroke-width:0.830522" />
<path
d="m 110.47,35.209 c -0.16044,0.45966 -0.21894,1.0471 -0.2406,1.3853 0.1148,-0.91063 0.93438,-2.0641 1.6282,-2.2375 -0.27755,0.05421 -1.1838,0.27327 -1.3876,0.85213"
fill="#2c2c2c"
id="path208"
style="stroke-width:0.830522" />
<g
fill="#1e1e1e"
id="g216"
style="stroke-width:0.830522">
<path
d="m 108.25,45.706 c 1.2856,-1.2034 3.3258,-2.4564 3.3258,-2.4564 l -0.3642,-0.4358 c -0.91283,0.68937 -2.1183,1.7192 -2.9616,2.8922"
id="path210"
style="stroke-width:0.830531" />
<path
d="m 110.34,10.456 c 0,0 0.24501,2.0185 3.677,2.0185 v -1.0537 c -2.1984,0 -2.9572,-0.45097 -3.677,-0.96484"
id="path212"
style="stroke-width:0.830531" />
<path
d="m 110.93,56.041 c 0.19067,0.02388 0.38146,0.04542 0.57445,0.06498 0,0 0.24501,-5.2576 0.54422,-7.3496 0.29922,-2.0943 0.98639,-3.768 0.98639,-3.768 l -1.2292,-1.4656 0.81077,1.524 c 0,0 -0.88236,2.3785 -1.0406,4.2321 -0.15615,1.8537 -0.64604,6.7621 -0.64604,6.7621"
id="path214"
style="stroke-width:0.830531" />
</g>
<g
fill="#2c2c2c"
id="g224"
style="stroke-width:0.830522">
<path
d="m 111.21,42.814 0.3642,0.4358 0.22983,0.27315 1.2292,1.4656 h 0.98651 v -2.528 c -0.75448,0 -1.392,-0.08885 -1.947,-0.26226 -0.22322,0.14955 -0.52244,0.3598 -0.86278,0.6157"
id="path218"
style="stroke-width:0.830531" />
<path
d="m 111.5,56.106 c 0.14098,0.0152 0.28184,0.02815 0.42282,0.03696 l 0.002,-0.03696 c -0.12128,-2.1768 0.34046,-6.9616 0.34046,-6.9616 0,0 0.0477,5.8733 0.87367,7.0721 0.29054,0.01085 0.58327,0.01516 0.87808,0.01516 v -11.243 h -0.9865 c 0,0 -0.68717,1.6737 -0.98639,3.768 -0.29922,2.0921 -0.54422,7.3496 -0.54422,7.3496"
id="path220"
style="stroke-width:0.830531" />
<path
d="m 110.83,37.288 c 0,0 1.5545,0.22334 3.1848,0.22334 v -0.14967 c 0,0 -0.59844,-0.39896 -0.80649,-0.41622 -0.21037,-0.01738 -1.5415,0.32737 -2.3783,0.34254"
id="path222"
style="stroke-width:0.830531" />
</g>
<g
fill="#1e1e1e"
id="g232"
style="stroke-width:0.830522">
<path
d="m 112.27,49.145 c 0,0 -0.46174,4.7848 -0.34045,6.9616 l -0.002,0.03696 c 0.40104,0.03453 0.80649,0.0606 1.2162,0.07358 -0.82594,-1.1989 -0.87367,-7.0721 -0.87367,-7.0721"
id="path226"
style="stroke-width:0.830531" />
<path
d="m 111.94,34.341 c 0,0 0.0932,0.56809 0.94746,0.75019 -0.1192,-0.21906 -0.11271,-0.42062 -0.11271,-0.42062 -0.4987,0.01946 -0.68729,-0.12789 -0.83475,-0.32957"
id="path228"
style="stroke-width:0.830531" />
<path
d="m 112.5,41.921 c -0.0217,0.01297 -0.17561,0.11051 -0.42282,0.27756 0.555,0.17341 1.1925,0.26226 1.947,0.26226 v -0.37289 c -0.29053,0 -0.91932,-0.02386 -1.5241,-0.16693"
id="path230"
style="stroke-width:0.830531" />
</g>
<path
d="m 114.02,38.394 c -1.2314,0 -1.7908,-0.06719 -1.8363,-0.07159 0.0607,0.03684 1.0254,0.62438 1.8363,0.62438 v -0.5528"
fill="#878787"
id="path234"
style="stroke-width:0.830522" />
<path
d="m 112.89,35.092 c 0.13878,0.25149 0.44669,0.52256 1.1274,0.52256 v -0.31439 c -0.79132,0 -1.2401,-0.62879 -1.2401,-0.62879 0,0 -0.007,0.20156 0.11271,0.42062"
fill="#1e1e1e"
id="path236"
style="stroke-width:0.830522" />
<path
d="m 111.05,37.405 c 0.4,0.18685 0.28775,0.51862 1.1027,0.51723 0.81494,-0.0014 1.6597,-0.12604 1.6597,-0.12604 l 1.9737,-0.22299 1.2146,-0.23956 -0.0483,-0.01842 c 0,0 -1.4287,0.19566 -2.931,0.19566 -1.5024,0 -3.1545,-0.21801 -3.1545,-0.21801 l 0.18315,0.11213"
fill="#fbfbfb"
id="path238"
style="stroke-width:0.830522" />
<path
d="m 115.39,37.619 c -0.34034,-2.31e-4 -0.79119,-0.0027 -1.2459,-0.01065 -0.91932,-0.01657 -1.628,-0.04564 -3.1612,-0.30061 -0.003,-3.61e-4 -0.006,-6.89e-4 -0.008,-0.0011 h -0.0834 l 0.16183,0.09904 c 0.39988,0.18685 0.28764,0.51862 1.1026,0.51723 0.81494,-0.0014 1.6597,-0.12604 1.6597,-0.12604 l 1.5749,-0.17793"
fill="#1e1e1e"
id="path240"
style="stroke-width:0.830522" />
<path
d="m 114.76,33.811 c 0.19843,-0.02491 0.30258,0.79872 0.0262,0.79826 -0.27651,-3.61e-4 -0.30327,-0.76339 -0.0262,-0.79826"
fill="#fbfbfb"
id="path242"
style="stroke-width:0.830522" />
<path
d="m 115.61,37.779 c 0.18801,-0.20689 0.68856,-0.07842 0.48237,0.05618 -0.2062,0.13449 -0.63516,0.11202 -0.48237,-0.05618"
fill="#fbfbfb"
id="path244"
style="stroke-width:0.830522" />
<g
fill="#1e1e1e"
id="g254"
style="stroke-width:0.830522">
<path
d="m 114.56,36.174 c 0.25334,-0.26493 0.75134,-0.17944 1.2784,0.0673 0.52708,0.24674 1.5588,0.62798 1.5139,0.74011 -0.0219,0.05479 -0.28022,0.01529 -0.3203,0.01251 -0.23759,-0.0168 -0.47448,-0.04645 -0.71045,-0.07854 -0.27524,-0.0373 -0.5506,-0.0746 -0.82398,-0.12349 -0.22172,-0.03962 -0.46719,-0.06893 -0.67594,-0.15615 -0.14098,-0.05896 -0.42317,-0.29273 -0.26168,-0.46174"
id="path246"
style="stroke-width:0.830531" />
<path
d="m 113.47,36.174 c -0.25334,-0.26493 -0.75135,-0.17944 -1.2784,0.0673 -0.52708,0.24674 -1.5586,0.62798 -1.5138,0.74011 0.0219,0.05479 0.28022,0.01529 0.32019,0.01251 0.23771,-0.0168 0.4746,-0.04645 0.71057,-0.07854 0.27524,-0.0373 0.55048,-0.0746 0.82398,-0.12349 0.2216,-0.03962 0.46719,-0.06893 0.67593,-0.15615 0.14086,-0.05896 0.42317,-0.29273 0.26157,-0.46174"
id="path248"
style="stroke-width:0.830531" />
<path
d="m 112.5,37.797 c 0.78796,0.06754 3.7608,-0.27478 4.7098,-0.50866 0,0 -0.82514,0.3532 -2.0576,0.41842 -1.2323,0.06533 -2.8111,0.44448 -2.8111,0.44448 l 0.15893,-0.35424"
id="path250"
style="stroke-width:0.830531" />
<path
d="m 110.83,37.288 c 0,0 0.30084,0.27616 0.69783,0.57133 l 0.16044,-0.12001 -0.72088,-0.4329 L 110.83,37.288"
id="path252"
style="stroke-width:0.830531" />
</g>
<path
d="m 113.19,38.811 c 0.72887,-0.06742 2.3548,-0.25798 2.3941,-0.12337 0.0393,0.13449 -0.96994,0.56067 -1.5307,0.56067 -0.56067,0 -0.86336,-0.4373 -0.86336,-0.4373"
fill="#2c2c2c"
id="path256"
style="stroke-width:0.830522" />
<path
d="m 116.74,37.258 c 0.0924,-0.04935 0.45097,-0.03024 0.51828,0.02502 0.0673,0.05537 0.40058,0.45375 0.19785,0.47958 C 117.37193,37.77337 116.74,37.258 116.74,37.258"
fill="#1e1e1e"
id="path258"
style="stroke-width:0.830522" />
<path
d="m 111.3,37.258 c -0.0924,-0.04935 -0.45108,-0.03024 -0.51827,0.02502 -0.0673,0.05537 -0.40069,0.45375 -0.19797,0.47958 0.0843,0.01077 0.71624,-0.5046 0.71624,-0.5046"
fill="#1e1e1e"
id="path260"
style="stroke-width:0.830522" />
<path
d="m 125.14,48.41 -2.0562,-4.8164 c 0,0 0.9419,3.1908 1.3009,3.7345 0.35877,0.54353 0.75529,1.082 0.75529,1.082"
fill="#878787"
id="path262"
style="stroke-width:0.830522" />
<path
d="m 101.2,42.088 c 0,0 1.5377,-2.5348 1.7444,-2.7826 0.20678,-0.24778 -0.33153,0.77614 -0.64546,1.4752 -0.31393,0.69922 -1.099,1.3074 -1.099,1.3074"
fill="#878787"
id="path264"
style="stroke-width:0.830522" />
</g>
<path
d="m 112.5,37.797 -3.3092,2.7712 c 0,0 0.0879,0.13519 0.0716,0.29146 l 3.2376,-2.8745 0.10044,-0.18222 -0.10044,-0.0059"
fill="#1f1f1f"
id="path268"
style="stroke-width:0.830522" />
<path
d="m 112.51,37.938 c -0.0941,0.26006 -2.6772,2.5031 -3.3217,3.131 v 0.0028 c -0.16311,0.15766 -0.26273,0.25439 -0.26273,0.25439 -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.33188,-0.45642 -0.12719,-0.68601 -0.12719,-0.68601 l 0.177,-0.12731 h 0.003 l 3.6755,-2.7496 c 0.37069,-0.12441 0.79131,0.31022 0.6779,0.6311"
fill="#2d2d2d"
id="path270"
style="stroke-width:0.830522" />
<path
d="m 108.55,39.952 3.2466,-2.4411 -2.9334,2.5915 z"
fill="#4e4e4e"
id="path272"
style="stroke-width:0.830522" />
<path
d="m 109.19,41.069 v 0.0028 c -0.16311,0.15766 -0.26273,0.25439 -0.26273,0.25439 -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.30964,-0.42606 -0.15198,-0.65288 -0.12997,-0.68312 l 0.003,-0.0029 0.17701,-0.12731 h 0.003 c 0.35691,-0.07727 1.0036,0.63215 1.0317,1.0125"
fill="#fbfdfe"
id="path274"
style="stroke-width:0.830522" />
<path
d="m 108.92,41.326 c -0.011,0 -0.49511,-0.0027 -0.82155,-0.4563 -0.30964,-0.42606 -0.15198,-0.65288 -0.12997,-0.68312 l 0.003,-0.0029 c 0.11619,-0.08294 0.49233,0.11063 0.6888,0.38992 0.19357,0.28219 0.34845,0.61697 0.25994,0.75239"
fill="#ef2e2e"
id="path276"
style="stroke-width:0.830522" />
<g
fill="#fbfafa"
stroke-width="0.830531"
id="g282">
<path
d="m 106.07,38.86 c -0.55696,-0.45398 -1.0123,-1.0714 -1.1242,-1.7814 -0.12858,-0.80904 0.19427,-1.6117 0.36919,-2.4117 0.17492,-0.80139 0.15048,-1.7636 -0.48364,-2.2831 -0.7576,-0.61998 -2.0464,-0.3254 -2.6831,-1.0689 -0.44379,-0.51966 -0.34347,-1.3107 -0.0952,-1.9474 0.24953,-0.63805 0.62381,-1.2477 0.66111,-1.9307 0.0463,-0.84634 -0.45665,-1.6528 -1.1306,-2.1673 -0.67524,-0.51457 -1.5023,-0.78216 -2.3242,-0.99566 -0.62902,-0.16334 -1.2876,-0.30999 -1.8008,-0.70999 -0.13692,-0.10657 -0.25462,-0.23215 -0.35714,-0.36919 0.15546,0.37672 0.39722,0.7159 0.68763,1.0006 0.31347,0.30744 0.67733,0.56357 1.0726,0.75494 0.44518,0.21558 0.92627,0.3481 1.3756,0.55476 0.44924,0.20666 0.88167,0.50634 1.1044,0.94804 0.27038,0.53623 0.1835,1.191 -0.0485,1.7448 0.0356,-0.43742 -0.21489,-0.85885 -0.55153,-1.1403 -0.7408,-0.61952 -1.9171,-0.94133 -2.8468,-1.1157 -0.41738,-0.07831 -0.85074,-0.12673 -1.2261,-0.3254 -0.37533,-0.19867 -0.68511,-0.59716 -0.62755,-1.0179 -0.20092,0.8117 -0.03264,1.6839 0.33462,2.4352 0.23976,0.49059 0.56087,0.93635 0.91708,1.3486 0.34185,0.3956 0.64477,0.58558 1.0887,0.83464 0.78425,0.44008 1.7538,0.74683 2.1923,1.6013 0.19936,0.38981 0.25462,0.85155 0.51966,1.2001 0.27397,0.36142 0.70872,0.52488 1.1396,0.69713 0.29447,0.08873 0.56716,0.22126 0.80382,0.41298 -0.23411,-0.18141 -0.51827,-0.29841 -0.80382,-0.41298 -0.54156,-0.16588 -1.15,-0.18905 -1.7223,-0.18256 -0.9609,0.01031 -2.0027,0.03857 -2.7796,-0.52743 -0.71138,-0.51839 -0.99821,-1.3776 -1.33,-2.2175 -0.27522,-0.46302 -0.69328,-0.83985 -1.2026,-1.0032 -0.67916,-0.21871 -1.5049,0 -1.9102,0.5865 -0.27395,0.39618 -0.33698,0.90032 -0.32924,1.3814 0.0077,0.49916 0.08747,1.0071 0.32802,1.4445 0.34215,0.62253 0.97882,1.0405 1.6528,1.2657 1.3094,0.4373 2.9533,0.48618 4.3231,0.44379 0.36143,-0.0117 0.72424,-0.03858 1.0857,-0.04773 0.74729,-0.01934 1.6284,-0.02827 2.2895,0.37058 0.94793,0.56982 0.56079,1.8959 0.57886,2.8092 0.0116,0.62635 0.10809,1.2721 0.47843,1.7917 0.70362,0.98523 2.0298,1.5217 3.2067,1.5936 0.4168,0.02583 0.88618,-0.07449 1.0097,-0.50414 -0.64304,-0.30234 -1.2914,-0.61106 -1.8419,-1.0599"
id="path278"
style="stroke-width:0.830531" />
<path
d="m 101.23,34.598 c 0.0467,0.0065 0.0932,0.01402 0.13994,0.02259 0.25056,0.04622 0.50472,0.13125 0.68647,0.30988 0.26644,0.26168 0.30999,0.64257 0.33386,0.99612 0.0273,0.40626 0.0195,0.83753 0.22809,1.1873 -0.057,-0.09545 -0.18859,-0.16982 -0.25485,-0.27095 -0.0778,-0.11874 -0.13322,-0.25056 -0.18315,-0.38286 -0.1,-0.26516 -0.16797,-0.54596 -0.30848,-0.79421 -0.35031,-0.61894 -1.1386,-0.65995 -1.7705,-0.53982 -0.53831,0.1024 -1.0681,0.28057 -1.5987,0.41147 -0.56924,0.1404 -1.1599,0.21674 -1.7445,0.25647 -0.27778,0.01877 -0.57598,0.02247 -0.81042,-0.12766 -0.19427,-0.12441 -0.31884,-0.35077 -0.31997,-0.58141 -0.0026,-0.5075 0.30223,-0.5572 0.6908,-0.48595 0.67532,0.12407 1.3504,0.10785 2.0336,0.08595 0.49545,-0.01564 0.99068,-0.04031 1.4852,-0.07391 0.46174,-0.03151 0.93159,-0.08816 1.3926,-0.01297"
id="path280"
style="stroke-width:0.830531" />
</g>
</g>
<rect
x="0.0054588001"
y="0.066145003"
width="137.58"
height="63.5"
fill="#333333"
fill-rule="evenodd"
opacity="0.56765"
id="rect286"
style="stroke-width:0.999978" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 368 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 57 KiB

49
desktop/CMakeLists.txt Normal file
View file

@ -0,0 +1,49 @@
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick QuickControls2 Sql)
find_package(ECM REQUIRED NO_MODULE)
find_package(KF6Baloo)
find_package(KF6FileMetaData)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
include(KDEInstallDirs)
include(ECMFindQmlModule)
include(ECMQmlModule)
qt_standard_project_setup()
qt_add_executable(com.github.joshstrobl.koto
config/config.cpp
config/library.cpp
config/ui_prefs.cpp
datalake/album.cpp
datalake/artist.cpp
datalake/cartographer.cpp
datalake/database.cpp
datalake/indexer.cpp
datalake/models.cpp
datalake/track.cpp
datalake/cartographer.hpp
datalake/structs.hpp
main.cpp
datalake/models.cpp
)
target_include_directories(com.github.joshstrobl.koto PUBLIC datalake includes)
ecm_add_qml_module(com.github.joshstrobl.koto URI "com.github.joshstrobl.koto" GENERATE_PLUGIN_SOURCE)
ecm_target_qml_sources(com.github.joshstrobl.koto
SOURCES
qml/PlayerBar/PlayerBar.qml
qml/PrimaryNavigation.qml
qml/HomePage.qml
qml/Main.qml
qml/Root.qml
)
target_link_libraries(com.github.joshstrobl.koto
PRIVATE KF6::Baloo KF6::FileMetaData Qt6::Quick Qt6::QuickControls2 Qt6::Sql
)
install(FILES com.github.joshstrobl.koto.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(TARGETS com.github.joshstrobl.koto ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
ecm_finalize_qml_module(com.github.joshstrobl.koto)

98
desktop/config/config.cpp Normal file
View file

@ -0,0 +1,98 @@
#include "config.hpp"
#include <QDir>
#include <QStandardPaths>
#include <QTextStream>
#include <filesystem>
namespace fs = std::filesystem;
KotoConfig::KotoConfig() {
// Define our application's config location
auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppConfigLocation));
auto configDirPath = configDir.absolutePath();
this->i_configDirPath = configDirPath;
this->i_libraries = QList<KotoLibraryConfig*>();
fs::path filePath {};
auto configPathStd = configDirPath.toStdString();
filePath /= configPathStd;
filePath /= "config.toml";
this->i_configPath = QString {filePath.c_str()};
if (QFileInfo::exists(i_configPath)) {
this->parseConfigFile(filePath);
} else {
this->bootstrap();
}
}
KotoConfig& KotoConfig::instance() {
static KotoConfig _instance;
return _instance;
}
void KotoConfig::bootstrap() {
this->i_uiPreferences = new KotoUiPreferences();
auto musicDir = QDir(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::MusicLocation));
auto musicLibrary = new KotoLibraryConfig("Music", musicDir.absolutePath().toStdString(), KotoLibraryType::Music);
this->i_libraries.append(musicLibrary);
this->save();
}
QString KotoConfig::getConfigDirPath() {
return QString {this->i_configDirPath};
}
KotoUiPreferences* KotoConfig::getUiPreferences() {
return this->i_uiPreferences;
}
QList<KotoLibraryConfig*> KotoConfig::getLibraries() {
return this->i_libraries;
}
void KotoConfig::parseConfigFile(std::string filePath) {
auto data = toml::parse(filePath);
std::optional<toml::value> ui_prefs;
if (data.contains("preferences.ui")) {
auto ui_prefs_at = data.at("preferences.ui");
if (ui_prefs_at.is_table()) ui_prefs = ui_prefs_at.as_table();
}
auto prefs = new KotoUiPreferences(ui_prefs);
this->i_uiPreferences = prefs;
for (const auto& lib_value : toml::find<std::vector<toml::value>>(data, "libraries")) {
auto lib = new KotoLibraryConfig(lib_value);
this->i_libraries.append(lib);
}
}
void KotoConfig::save() {
toml::ordered_value config_table(toml::ordered_table {});
config_table["preferences.ui"] = this->i_uiPreferences->serialize();
toml::ordered_value libraries_array(toml::ordered_array {});
for (auto lib : this->i_libraries) {
auto lib_table = lib->serialize();
libraries_array.push_back(lib_table);
}
config_table["libraries"] = libraries_array;
auto configContent = toml::format(config_table);
auto config_dir = QDir {this->i_configDirPath};
if (!config_dir.exists()) config_dir.mkpath(".");
auto config_file = QFile {this->i_configPath};
auto out = QTextStream {&config_file};
if (config_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
out << configContent.c_str();
config_file.close();
}
}

28
desktop/config/config.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <QList>
#include <QString>
#include "library.hpp"
#include "ui_prefs.hpp"
class KotoConfig {
public:
KotoConfig();
static KotoConfig& instance();
static KotoConfig* create() { return &instance(); }
void save();
QString getConfigDirPath();
QList<KotoLibraryConfig*> getLibraries();
KotoUiPreferences* getUiPreferences();
private:
void bootstrap();
void parseConfigFile(std::string filePath);
QString i_configDirPath;
QString i_configPath;
QList<KotoLibraryConfig*> i_libraries;
KotoUiPreferences* i_uiPreferences;
};

View file

@ -0,0 +1,60 @@
#include "library.hpp"
#include <QDebug>
#include <string>
KotoLibraryConfig::KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type) {
this->i_name = name;
this->i_path = path;
this->i_type = type;
qDebug() << "Library: " << this->i_name.c_str() << " at " << this->i_path.c_str();
}
KotoLibraryConfig::~KotoLibraryConfig() {}
KotoLibraryConfig::KotoLibraryConfig(const toml::value& v) {
this->i_name = toml::find<std::string>(v, "name");
this->i_path = toml::find<std::string>(v, "path");
this->i_type = libraryTypeFromString(toml::find<std::string>(v, "type"));
}
std::string KotoLibraryConfig::getName() {
return this->i_name;
}
fs::path KotoLibraryConfig::getPath() {
return this->i_path;
}
KotoLibraryType KotoLibraryConfig::getType() {
return this->i_type;
}
toml::ordered_value KotoLibraryConfig::serialize() {
toml::ordered_value library_table(toml::ordered_table {});
library_table["name"] = this->i_name;
library_table["path"] = this->i_path.string();
auto stringifiedType = libraryTypeToString(this->i_type);
library_table["type"] = stringifiedType;
return library_table;
}
std::string libraryTypeToString(KotoLibraryType type) {
switch (type) {
case KotoLibraryType::Audiobooks:
return std::string {"audiobooks"};
case KotoLibraryType::Music:
return std::string {"music"};
case KotoLibraryType::Podcasts:
return std::string {"podcasts"};
default:
return std::string {"unknown"};
}
}
KotoLibraryType libraryTypeFromString(const std::string& type) {
if (type == "audiobooks") return KotoLibraryType::Audiobooks;
if (type == "music") return KotoLibraryType::Music;
if (type == "podcasts") return KotoLibraryType::Podcasts;
throw std::invalid_argument("Unknown KotoLibraryType: " + type);
}

View file

@ -0,0 +1,32 @@
#pragma once
#include <filesystem>
#include <string>
#include "includes/toml.hpp"
namespace fs = std::filesystem;
enum class KotoLibraryType {
Audiobooks,
Music,
Podcasts,
};
KotoLibraryType libraryTypeFromString(const std::string& type);
std::string libraryTypeToString(KotoLibraryType type);
class KotoLibraryConfig {
public:
KotoLibraryConfig(std::string name, fs::path path, KotoLibraryType type);
KotoLibraryConfig(const toml::value& v);
~KotoLibraryConfig();
std::string getName();
fs::path getPath();
KotoLibraryType getType();
toml::ordered_value serialize();
private:
std::string i_name;
fs::path i_path;
KotoLibraryType i_type;
};

View file

@ -0,0 +1,71 @@
#include "ui_prefs.hpp"
KotoUiPreferences::KotoUiPreferences()
: i_albumInfoShowDescription(true), i_albumInfoShowGenre(true), i_albumInfoShowNarrator(true), i_albumInfoShowYear(true), i_lastUsedVolume(0.5) {}
KotoUiPreferences::KotoUiPreferences(std::optional<toml::value> v) {
// No UI prefs provided
if (!v.has_value()) return;
toml::value& uiPrefs = v.value();
auto showDescription = toml::find_or<bool>(uiPrefs, "album_info_show_description", false);
auto showGenre = toml::find_or<bool>(uiPrefs, "album_info_show_genre", false);
auto showNarrator = toml::find_or<bool>(uiPrefs, "album_info_show_narrator", false);
auto showYear = toml::find_or<bool>(uiPrefs, "album_info_show_year", false);
auto lastUsedVolume = toml::find_or<float>(uiPrefs, "last_used_volume", 0.5);
this->setAlbumInfoShowDescription(showDescription);
this->setAlbumInfoShowGenre(showGenre);
this->setAlbumInfoShowNarrator(showNarrator);
this->setAlbumInfoShowYear(showYear);
this->setLastUsedVolume(lastUsedVolume);
}
bool KotoUiPreferences::getAlbumInfoShowDescription() {
return this->i_albumInfoShowDescription;
}
bool KotoUiPreferences::getAlbumInfoShowGenre() {
return this->i_albumInfoShowGenre;
}
bool KotoUiPreferences::getAlbumInfoShowNarrator() {
return this->i_albumInfoShowNarrator;
}
bool KotoUiPreferences::getAlbumInfoShowYear() {
return this->i_albumInfoShowYear;
}
float KotoUiPreferences::getLastUsedVolume() {
return this->i_lastUsedVolume;
}
toml::ordered_value KotoUiPreferences::serialize() {
toml::ordered_value ui_prefs_table(toml::ordered_table {});
ui_prefs_table["album_info_show_description"] = this->i_albumInfoShowDescription;
ui_prefs_table["album_info_show_genre"] = this->i_albumInfoShowGenre;
ui_prefs_table["album_info_show_narrator"] = this->i_albumInfoShowNarrator;
ui_prefs_table["album_info_show_year"] = this->i_albumInfoShowYear;
ui_prefs_table["last_used_volume"] = this->i_lastUsedVolume;
return ui_prefs_table;
}
void KotoUiPreferences::setAlbumInfoShowDescription(bool show) {
this->i_albumInfoShowDescription = show;
}
void KotoUiPreferences::setAlbumInfoShowGenre(bool show) {
this->i_albumInfoShowGenre = show;
}
void KotoUiPreferences::setAlbumInfoShowNarrator(bool show) {
this->i_albumInfoShowNarrator = show;
}
void KotoUiPreferences::setAlbumInfoShowYear(bool show) {
this->i_albumInfoShowYear = show;
}
void KotoUiPreferences::setLastUsedVolume(float volume) {
this->i_lastUsedVolume = volume;
}

View file

@ -0,0 +1,35 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "includes/toml.hpp"
class KotoUiPreferences {
public:
KotoUiPreferences();
KotoUiPreferences(std::optional<toml::value> v);
~KotoUiPreferences();
bool getAlbumInfoShowDescription();
bool getAlbumInfoShowGenre();
bool getAlbumInfoShowNarrator();
bool getAlbumInfoShowYear();
float getLastUsedVolume();
toml::ordered_value serialize();
void setAlbumInfoShowDescription(bool show);
void setAlbumInfoShowGenre(bool show);
void setAlbumInfoShowNarrator(bool show);
void setAlbumInfoShowYear(bool show);
void setLastUsedVolume(float volume);
private:
bool i_albumInfoShowDescription;
bool i_albumInfoShowGenre;
bool i_albumInfoShowNarrator;
bool i_albumInfoShowYear;
float i_lastUsedVolume;
};

119
desktop/datalake/album.cpp Normal file
View file

@ -0,0 +1,119 @@
#include <iostream>
#include "database.hpp"
#include "structs.hpp"
KotoAlbum::KotoAlbum() {
this->uuid = QUuid::createUuid();
this->tracks = QList<KotoTrack*>();
}
KotoAlbum* KotoAlbum::fromDb(const QSqlQuery& query, const QSqlRecord& record) {
KotoAlbum* album = new KotoAlbum();
album->uuid = QUuid {query.value(record.indexOf("id")).toString()};
album->artist_uuid = QUuid {query.value(record.indexOf("artist_id")).toString()};
album->title = QString {query.value(record.indexOf("name")).toString()};
album->year = query.value(record.indexOf("year")).toInt();
album->description = QString {query.value(record.indexOf("description")).toString()};
album->narrator = QString {query.value(record.indexOf("narrator")).toString()};
album->album_art_path = QString {query.value(record.indexOf("art_path")).toString()};
album->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")};
return album;
}
KotoAlbum::~KotoAlbum() {
for (auto track : this->tracks) { delete track; }
this->tracks.clear();
}
void KotoAlbum::addTrack(KotoTrack* track) {
this->tracks.append(track);
}
void KotoAlbum::commit() {
QSqlQuery query(KotoDatabase::instance().getDatabase());
query.prepare(
"INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year) "
"VALUES (:id, :artist_id, :name, :description, :narrator, :art_path, :genres, :year) "
"ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, name = :name, description = :description, narrator = :narrator, art_path = "
":art_path, genres = :genres, year = :year");
query.bindValue(":id", this->uuid.toString());
query.bindValue(":artist_id", this->artist_uuid.toString());
query.bindValue(":name", this->title);
query.bindValue(":year", this->year.value_or(NULL));
query.bindValue(":description", this->description);
query.bindValue(":art_path", this->album_art_path);
query.bindValue(":narrator", this->narrator);
query.bindValue(":genres", this->genres.join(", "));
query.exec();
}
QString KotoAlbum::getAlbumArtPath() {
return QString {this->album_art_path};
}
QString KotoAlbum::getDescription() {
return QString {this->description};
}
QList<QString> KotoAlbum::getGenres() {
return QList {this->genres};
}
QString KotoAlbum::getPath() {
return this->path;
}
QString KotoAlbum::getNarrator() {
return QString {this->narrator};
}
QString KotoAlbum::getTitle() {
return QString {this->title};
}
QList<KotoTrack*> KotoAlbum::getTracks() {
return QList {this->tracks};
}
std::optional<int> KotoAlbum::getYear() {
return this->year;
}
int KotoAlbum::getYearQml() {
return this->year.value_or(0);
}
void KotoAlbum::removeTrack(KotoTrack* track) {
this->tracks.removeOne(track);
}
void KotoAlbum::setAlbumArtPath(QString str) {
this->album_art_path = QString {path};
}
void KotoAlbum::setDescription(QString str) {
this->description = QString {str};
}
void KotoAlbum::setGenres(QList<QString> list) {
this->genres = QList {list};
}
void KotoAlbum::setNarrator(QString str) {
this->narrator = QString {str};
}
void KotoAlbum::setPath(QString str) {
this->path = QString {str};
}
void KotoAlbum::setTitle(QString str) {
this->title = QString {str};
}
void KotoAlbum::setYear(int num) {
this->year = num;
}

View file

@ -0,0 +1,91 @@
#include <QSqlQuery>
#include "database.hpp"
#include "structs.hpp"
KotoArtist::KotoArtist() {
this->uuid = QUuid::createUuid();
}
KotoArtist* KotoArtist::fromDb(const QSqlQuery& query, const QSqlRecord& record) {
KotoArtist* artist = new KotoArtist();
artist->uuid = QUuid {query.value(record.indexOf("id")).toString()};
artist->name = QString {query.value(record.indexOf("name")).toString()};
artist->path = QString {query.value(record.indexOf("art_path")).toString()};
return artist;
}
KotoArtist::~KotoArtist() {
for (auto album : this->albums) { delete album; }
for (auto track : this->tracks) { delete track; }
this->albums.clear();
this->tracks.clear();
}
void KotoArtist::addAlbum(KotoAlbum* album) {
this->albums.append(album);
}
void KotoArtist::addTrack(KotoTrack* track) {
this->tracks.append(track);
if (!track->album_uuid.has_value()) return;
for (auto album : this->albums) {
if (album->uuid == track->album_uuid.value()) {
album->addTrack(track);
return;
}
}
}
void KotoArtist::commit() {
QSqlQuery query(KotoDatabase::instance().getDatabase());
query.prepare("INSERT INTO artists(id, name, art_path) VALUES (:id, :name, :art_path) ON CONFLICT(id) DO UPDATE SET name = :name, art_path = :art_path");
query.bindValue(":id", this->uuid.toString());
query.bindValue(":name", this->name);
query.bindValue(":art_path", this->path);
query.exec();
}
QList<KotoAlbum*> KotoArtist::getAlbums() {
return QList {this->albums};
}
std::optional<KotoAlbum*> KotoArtist::getAlbumByName(QString name) {
for (auto album : this->albums) {
if (album->getTitle().contains(name)) { return std::optional {album}; }
}
return std::nullopt;
}
QString KotoArtist::getName() {
return QString {this->name};
}
QString KotoArtist::getPath() {
return QString {this->path};
}
QList<KotoTrack*> KotoArtist::getTracks() {
return QList {this->tracks};
}
QUuid KotoArtist::getUuid() {
return this->uuid;
}
void KotoArtist::removeAlbum(KotoAlbum* album) {
this->albums.removeOne(album);
}
void KotoArtist::removeTrack(KotoTrack* track) {
this->tracks.removeOne(track);
}
void KotoArtist::setName(QString str) {
this->name = QString {str};
}
void KotoArtist::setPath(QString str) {
this->path = QString {str};
}

View file

@ -0,0 +1,63 @@
#include "cartographer.hpp"
#include <iostream>
Cartographer::Cartographer(QObject* parent)
: QObject(parent),
i_albums(QHash<QUuid, KotoAlbum*>()),
i_artists_model(new KotoArtistModel(QList<KotoArtist*>())),
i_artists_by_name(QHash<QString, KotoArtist*>()),
i_tracks(QHash<QUuid, KotoTrack*>()) {}
Cartographer& Cartographer::instance() {
static Cartographer _instance(nullptr);
return _instance;
}
void Cartographer::addAlbum(KotoAlbum* album) {
this->i_albums.insert(album->uuid, album);
}
void Cartographer::addArtist(KotoArtist* artist) {
this->i_artists_model->addArtist(artist);
this->i_artists_by_name.insert(artist->getName(), artist);
}
void Cartographer::addTrack(KotoTrack* track) {
this->i_tracks.insert(track->uuid, track);
}
std::optional<KotoAlbum*> Cartographer::getAlbum(QUuid uuid) {
auto album = this->i_albums.value(uuid, nullptr);
return album ? std::optional {album} : std::nullopt;
}
std::optional<KotoArtist*> Cartographer::getArtist(QUuid uuid) {
for (auto artist : this->i_artists_model->getArtists()) {
if (artist->uuid == uuid) { return std::optional {artist}; }
}
return std::nullopt;
}
QList<KotoArtist*> Cartographer::getArtists() {
return this->i_artists_model->getArtists();
}
KotoArtistModel* Cartographer::getArtistsModel() {
// if (this->i_artists_model == nullptr) { this->i_artists_model = new KotoArtistModel(this->i_artists); }
return this->i_artists_model;
}
std::optional<KotoArtist*> Cartographer::getArtist(QString name) {
auto artist = this->i_artists_by_name.value(name, nullptr);
return artist ? std::optional {artist} : std::nullopt;
}
std::optional<KotoTrack*> Cartographer::getTrack(QUuid uuid) {
auto track = this->i_tracks.value(uuid, nullptr);
return track ? std::optional {track} : std::nullopt;
}
QList<KotoTrack*> Cartographer::getTracks() {
return this->i_tracks.values();
}

View file

@ -0,0 +1,51 @@
#pragma once
#include <QtQml/qqmlregistration.h>
#include <QHash>
#include <QQmlEngine>
#include <QQmlListProperty>
#include <QString>
#include <QUuid>
#include <optional>
#include "structs.hpp"
class Cartographer : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
// Q_PROPERTY(QQmlListProperty<KotoAlbum*> albums READ getAlbumsQml)
Q_PROPERTY(KotoArtistModel* artists READ getArtistsModel CONSTANT)
// Q_PROPERTY(QQmlListProperty<KotoTrack*> tracks READ getTracksQml)
public:
Cartographer(QObject* parent);
static Cartographer& instance();
static Cartographer* create(QQmlEngine*, QJSEngine*) {
QQmlEngine::setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
void addAlbum(KotoAlbum* album);
void addArtist(KotoArtist* artist);
void addTrack(KotoTrack* track);
// QQmlListProperty<KotoAlbum*> getAlbumsQml();
KotoArtistModel* getArtistsModel();
// QQmlListProperty<KotoTrack*> getTracksQml();
std::optional<KotoAlbum*> getAlbum(QUuid uuid);
QList<KotoAlbum*> getAlbums();
std::optional<KotoArtist*> getArtist(QUuid uuid);
QList<KotoArtist*> getArtists();
std::optional<KotoArtist*> getArtist(QString name);
std::optional<KotoTrack*> getTrack(QUuid uuid);
QList<KotoTrack*> getTracks();
private:
QHash<QUuid, KotoAlbum*> i_albums;
KotoArtistModel* i_artists_model;
QHash<QString, KotoArtist*> i_artists_by_name;
QHash<QUuid, KotoTrack*> i_tracks;
};

View file

@ -0,0 +1,102 @@
#include "database.hpp"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QSqlQuery>
#include <iostream>
#include "cartographer.hpp"
#include "config/config.hpp"
KotoDatabase::KotoDatabase() {
QString dbPath = QDir(KotoConfig::instance().getConfigDirPath()).filePath("koto.db");
this->shouldBootstrap = !QFileInfo::exists(dbPath);
this->db = QSqlDatabase::addDatabase("QSQLITE");
std::cout << "Database path: " << dbPath.toStdString() << std::endl;
this->db.setDatabaseName(dbPath);
}
KotoDatabase& KotoDatabase::instance() {
static KotoDatabase _instance;
return _instance;
}
void KotoDatabase::connect() {
if (!this->db.open()) {
std::cerr << "Failed to open database" << std::endl;
QCoreApplication::quit();
}
if (this->shouldBootstrap) this->bootstrap();
}
void KotoDatabase::disconnect() {
this->db.close();
}
QSqlDatabase KotoDatabase::getDatabase() {
return this->db;
}
bool KotoDatabase::requiredBootstrap() {
return this->shouldBootstrap;
}
void KotoDatabase::bootstrap() {
QSqlQuery query(this->db);
query.exec("CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);");
query.exec(
"CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, "
"genres strings, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);");
query.exec(
"CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, "
"genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE, FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE);");
query.exec(
"CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES "
"albums(id) "
"ON DELETE CASCADE);");
query.exec(
"CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES "
"artists(id) ON DELETE CASCADE);");
query.exec(
"CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_id) FOREIGN KEY(track_id) REFERENCES "
"tracks(id) "
"ON DELETE CASCADE);");
query.exec(
"CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int, album_id string, track_id "
"string, "
"playback_position_of_track int);");
query.exec(
"CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) "
"REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);");
}
void KotoDatabase::load() {
QSqlQuery query(this->db);
query.exec("SELECT * FROM artists;");
while (query.next()) {
KotoArtist* artist = KotoArtist::fromDb(query, query.record());
Cartographer::instance().addArtist(artist);
}
query.exec("SELECT * FROM albums;");
while (query.next()) {
KotoAlbum* album = KotoAlbum::fromDb(query, query.record());
auto artist = Cartographer::instance().getArtist(album->artist_uuid);
if (artist.has_value()) { artist.value()->addAlbum(album); }
Cartographer::instance().addAlbum(album);
}
query.exec("SELECT * FROM tracks;");
while (query.next()) {
KotoTrack* track = KotoTrack::fromDb(query, query.record());
auto artist = Cartographer::instance().getArtist(track->artist_uuid);
if (artist.has_value()) { artist.value()->addTrack(track); }
Cartographer::instance().addTrack(track);
}
}

View file

@ -0,0 +1,21 @@
#pragma once
#include <QSqlDatabase>
class KotoDatabase {
public:
KotoDatabase();
static KotoDatabase& instance();
static KotoDatabase* create() { return &instance(); }
void connect();
void disconnect();
QSqlDatabase getDatabase();
void load();
bool requiredBootstrap();
private:
void bootstrap();
bool shouldBootstrap;
QSqlDatabase db;
};

View file

@ -0,0 +1,88 @@
#include "indexer.hpp"
#include <KFileMetaData/ExtractorCollection>
#include <QDebug>
#include <QDirIterator>
#include <QMimeDatabase>
#include <iostream>
FileIndexer::FileIndexer(KotoLibraryConfig* config) {
this->i_root = QString {config->getPath().c_str()};
}
FileIndexer::~FileIndexer() = default;
void FileIndexer::index() {
QMimeDatabase db;
KFileMetaData::ExtractorCollection extractors;
QStringList root_dirs {this->i_root.split(QDir::separator())};
QDirIterator it {this->i_root, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories};
while (it.hasNext()) {
QString path = it.next();
QFileInfo info {path};
if (info.isDir()) {
auto diffPath = info.dir().relativeFilePath(this->i_root);
auto diffDirs = diffPath.split("..");
auto diffDirsSize = diffDirs.size() - 1;
// This is going to be an artist
if (diffDirsSize == 0) {
auto artist = new KotoArtist();
artist->setName(info.fileName());
artist->setPath(path);
artist->commit();
this->i_artists.append(artist);
Cartographer::instance().addArtist(artist);
continue;
} else if (diffDirsSize == 1) {
auto album = new KotoAlbum();
album->setTitle(info.fileName());
auto artistDir = QDir(info.dir());
auto artistName = artistDir.dirName();
auto artistOptional = Cartographer::instance().getArtist(artistName);
if (artistOptional.has_value()) {
auto artist = artistOptional.value();
album->artist_uuid = artist->uuid;
artist->addAlbum(album);
}
album->commit();
Cartographer::instance().addAlbum(album);
continue;
}
}
// This is a file
QMimeType mime = db.mimeTypeForFile(info);
if (mime.name().startsWith("audio/")) {
auto extractorList = extractors.fetchExtractors(mime.name());
if (extractorList.isEmpty()) { continue; }
auto result = KFileMetaData::SimpleExtractionResult(path, mime.name(), KFileMetaData::ExtractionResult::ExtractMetaData);
extractorList.first()->extract(&result);
if (!result.types().contains(KFileMetaData::Type::Audio)) { continue; }
auto track = KotoTrack::fromMetadata(result, info);
this->i_tracks.append(track);
track->commit();
Cartographer::instance().addTrack(track);
} else if (mime.name().startsWith("image/")) {
// This is an image, TODO add cover art to album
}
}
}
void indexAllLibraries() {
for (auto library : KotoConfig::instance().getLibraries()) {
auto indexer = new FileIndexer(library);
indexer->index();
}
}

View file

@ -0,0 +1,26 @@
#pragma once
#include <string>
#include "cartographer.hpp"
#include "config/config.hpp"
#include "structs.hpp"
class FileIndexer {
public:
FileIndexer(KotoLibraryConfig* config);
~FileIndexer();
QList<KotoArtist*> getArtists();
QList<KotoTrack*> getFiles();
QString getRoot();
void index();
protected:
QList<KotoArtist*> i_artists;
QList<KotoTrack*> i_tracks;
QString i_root;
};
void indexAllLibraries();

View file

@ -0,0 +1,49 @@
#include "structs.hpp"
KotoArtistModel::KotoArtistModel(const QList<KotoArtist*>& artists, QObject* parent) : QAbstractListModel(parent), m_artists(artists) {}
KotoArtistModel::~KotoArtistModel() {
this->beginResetModel();
this->m_artists.clear();
this->endResetModel();
}
void KotoArtistModel::addArtist(KotoArtist* artist) {
this->beginInsertRows(QModelIndex(), this->m_artists.count(), this->m_artists.count());
this->m_artists.append(artist);
this->endInsertRows();
}
int KotoArtistModel::rowCount(const QModelIndex& parent) const {
return this->m_artists.count();
}
QVariant KotoArtistModel::data(const QModelIndex& index, int role) const {
if (!index.isValid()) { return {}; }
if (index.row() >= this->m_artists.size()) { return {}; }
if (role == Qt::DisplayRole) {
return this->m_artists.at(index.row())->getName();
} else if (role == KotoArtistRoles::NameRole) {
return this->m_artists.at(index.row())->getName();
} else if (role == KotoArtistRoles::PathRole) {
return this->m_artists.at(index.row())->getPath();
} else if (role == KotoArtistRoles::UuidRole) {
return this->m_artists.at(index.row())->uuid;
} else {
return {};
}
}
QList<KotoArtist*> KotoArtistModel::getArtists() {
return this->m_artists;
}
QHash<int, QByteArray> KotoArtistModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[NameRole] = QByteArrayLiteral("name");
roles[PathRole] = QByteArrayLiteral("path");
roles[UuidRole] = QByteArrayLiteral("uuid");
return roles;
}

View file

@ -0,0 +1,227 @@
#pragma once
#include <QtQml/qqmlregistration.h>
#include <KFileMetaData/SimpleExtractionResult>
#include <QAbstractListModel>
#include <QFileInfo>
#include <QList>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QString>
#include <QUuid>
class KotoArtist;
class KotoArtistModel;
class KotoAlbum;
class KotoAlbumModel;
class KotoTrack;
class KotoTrackModel;
class KotoArtist : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QList<KotoAlbum*> albums READ getAlbums NOTIFY albumsChanged)
Q_PROPERTY(QList<KotoTrack*> tracks READ getTracks NOTIFY tracksChanged)
Q_PROPERTY(QUuid uuid READ getUuid)
public:
KotoArtist();
static KotoArtist* fromDb(const QSqlQuery& query, const QSqlRecord& record);
virtual ~KotoArtist();
QUuid uuid;
void addAlbum(KotoAlbum* album);
void addTrack(KotoTrack* track);
void commit();
QList<KotoAlbum*> getAlbums();
std::optional<KotoAlbum*> getAlbumByName(QString name);
QString getName();
QString getPath();
QList<KotoTrack*> getTracks();
QUuid getUuid();
void removeAlbum(KotoAlbum* album);
void removeTrack(KotoTrack* track);
void setName(QString str);
void setPath(QString str);
signals:
void albumsChanged();
void nameChanged();
void pathChanged();
void tracksChanged();
private:
QString path;
QString name;
QList<KotoAlbum*> albums;
QList<KotoTrack*> tracks;
};
class KotoArtistModel : public QAbstractListModel {
Q_OBJECT
public:
explicit KotoArtistModel(const QList<KotoArtist*>& artists, QObject* parent = nullptr);
void addArtist(KotoArtist* artist);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
virtual ~KotoArtistModel();
QList<KotoArtist*> getArtists();
enum KotoArtistRoles {
NameRole = Qt::UserRole + 1,
PathRole,
UuidRole,
};
private:
QList<KotoArtist*> m_artists;
};
class KotoAlbum : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString albumArtPath READ getAlbumArtPath WRITE setAlbumArtPath NOTIFY albumArtChanged)
Q_PROPERTY(QString description READ getDescription WRITE setDescription NOTIFY descriptionChanged)
Q_PROPERTY(QList<QString> genres READ getGenres WRITE setGenres NOTIFY genresChanged)
Q_PROPERTY(QString narrator READ getNarrator WRITE setNarrator NOTIFY narratorChanged)
Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
Q_PROPERTY(QList<KotoTrack*> tracks READ getTracks NOTIFY tracksChanged)
Q_PROPERTY(int year READ getYearQml WRITE setYear NOTIFY yearChanged)
public:
KotoAlbum();
static KotoAlbum* fromDb(const QSqlQuery& query, const QSqlRecord& record);
virtual ~KotoAlbum();
QUuid uuid;
QUuid artist_uuid;
void commit();
QString getAlbumArtPath();
QString getDescription();
QList<QString> getGenres();
QString getNarrator();
QString getPath();
QString getTitle();
QList<KotoTrack*> getTracks();
std::optional<int> getYear();
int getYearQml();
void addTrack(KotoTrack* track);
void removeTrack(KotoTrack* track);
void setAlbumArtPath(QString str);
void setDescription(QString str);
void setGenres(QList<QString> list);
void setNarrator(QString str);
void setPath(QString str);
void setTitle(QString str);
void setYear(int num);
signals:
void albumArtChanged();
void descriptionChanged();
void genresChanged();
void narratorChanged();
void pathChanged();
void titleChanged();
void tracksChanged();
void yearChanged();
private:
QString title;
QString description;
QString narrator;
std::optional<int> year;
QList<QString> genres;
QList<KotoTrack*> tracks;
QString path;
QString album_art_path;
};
class KotoTrack : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString album_uuid READ getAlbumUuid NOTIFY albumChanged)
Q_PROPERTY(QUuid artist_uuid READ getArtistUuid NOTIFY artistChanged)
Q_PROPERTY(QUuid uuid READ getUuid)
Q_PROPERTY(int discNumber READ getDiscNumber WRITE setDiscNumber NOTIFY discNumberChanged)
Q_PROPERTY(int duration READ getDuration WRITE setDuration NOTIFY durationChanged)
Q_PROPERTY(QStringList genres READ getGenres WRITE setGenres NOTIFY genresChanged)
Q_PROPERTY(QString lyrics READ getLyrics WRITE setLyrics NOTIFY lyricsChanged)
Q_PROPERTY(QString narrator READ getNarrator WRITE setNarrator NOTIFY narratorChanged)
Q_PROPERTY(QString path READ getPath WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
Q_PROPERTY(int trackNumber READ getTrackNumber WRITE setTrackNumber NOTIFY trackNumberChanged)
Q_PROPERTY(int year READ getYear WRITE setYear NOTIFY yearChanged)
public:
KotoTrack(); // No-op constructor
static KotoTrack* fromDb(const QSqlQuery& query, const QSqlRecord& record);
static KotoTrack* fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info);
virtual ~KotoTrack();
std::optional<QUuid> album_uuid;
QUuid artist_uuid;
QUuid uuid;
void commit();
QString getAlbumUuid();
QUuid getArtistUuid();
int getDiscNumber();
int getDuration();
QStringList getGenres();
QString getLyrics();
QString getNarrator();
QString getPath();
QString getTitle();
int getTrackNumber();
QUuid getUuid();
int getYear();
void setAlbum(KotoAlbum* album);
void setArtist(KotoArtist* artist);
void setDiscNumber(int num);
void setDuration(int num);
void setGenres(QList<QString> list);
void setLyrics(QString str);
void setNarrator(QString str);
void setPath(QString path);
void setTitle(QString str);
void setTrackNumber(int num);
void setYear(int num);
signals:
void albumChanged();
void artistChanged();
void discNumberChanged();
void durationChanged();
void genresChanged();
void lyricsChanged();
void narratorChanged();
void pathChanged();
void titleChanged();
void trackNumberChanged();
void yearChanged();
private:
int disc_number;
int duration;
QStringList genres;
QString lyrics;
QString narrator;
QString path;
QString title;
int track_number;
int year;
};

198
desktop/datalake/track.cpp Normal file
View file

@ -0,0 +1,198 @@
#include <QFileInfo>
#include <iostream>
#include "cartographer.hpp"
#include "database.hpp"
#include "structs.hpp"
KotoTrack::KotoTrack() {
this->uuid = QUuid::createUuid();
}
KotoTrack* KotoTrack::fromDb(const QSqlQuery& query, const QSqlRecord& record) {
KotoTrack* track = new KotoTrack();
track->uuid = QUuid {query.value(record.indexOf("id")).toString()};
auto artist_id = query.value(record.indexOf("artist_id"));
if (!artist_id.isNull()) { track->artist_uuid = QUuid {artist_id.toString()}; }
auto album_id = query.value(record.indexOf("album_id"));
if (!album_id.isNull()) { track->album_uuid = QUuid {album_id.toString()}; }
track->title = QString {query.value(record.indexOf("name")).toString()};
track->disc_number = query.value(record.indexOf("disc")).toInt();
track->track_number = query.value(record.indexOf("position")).toInt();
track->duration = query.value(record.indexOf("duration")).toInt();
track->genres = QList {query.value(record.indexOf("genres")).toString().split(", ")};
return track;
}
KotoTrack* KotoTrack::fromMetadata(const KFileMetaData::SimpleExtractionResult& metadata, const QFileInfo& info) {
auto props = metadata.properties();
KotoTrack* track = new KotoTrack();
track->disc_number = props.value(KFileMetaData::Property::DiscNumber, 0).toInt();
track->duration = props.value(KFileMetaData::Property::Duration, 0).toInt();
QStringList genres;
for (auto v : props.values(KFileMetaData::Property::Genre)) { genres.append(v.toString()); }
track->genres = genres;
track->lyrics = props.value(KFileMetaData::Property::Lyrics).toString();
track->narrator = props.value(KFileMetaData::Property::Performer).toString();
track->path = info.absolutePath();
track->track_number = props.value(KFileMetaData::Property::TrackNumber, 0).toInt();
track->year = props.value(KFileMetaData::Property::ReleaseYear, 0).toInt();
auto titleResult = props.value(KFileMetaData::Property::Title);
if (titleResult.isValid() && !titleResult.isNull()) {
track->title = titleResult.toString();
} else {
// TODO: mirror the same logic we had for cleaning up file name to determine track name, position, chapter, artist, etc.
track->title = info.fileName();
}
auto artistResult = props.value(KFileMetaData::Property::Artist);
auto artistOptional = std::optional<KotoArtist*>();
if (artistResult.isValid()) {
artistOptional = Cartographer::instance().getArtist(artistResult.toString());
if (artistOptional.has_value()) {
auto artist = artistOptional.value();
track->artist_uuid = QUuid(artist->uuid);
artist->addTrack(track);
}
}
auto albumResult = props.value(KFileMetaData::Property::Album);
if (albumResult.isValid() && artistOptional.has_value()) {
auto artist = artistOptional.value();
auto albumMetaName = albumResult.toString();
auto albumOptional = artist->getAlbumByName(albumMetaName);
if (albumOptional.has_value()) {
auto album = albumOptional.value();
track->album_uuid = QUuid(album->uuid);
album->addTrack(track);
if (album->getTitle() != albumMetaName) album->setTitle(albumMetaName);
}
}
return track;
}
KotoTrack::~KotoTrack() {}
void KotoTrack::commit() {
QSqlQuery query(KotoDatabase::instance().getDatabase());
query.prepare(
"INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres) "
"VALUES (:id, :artist_id, :album_id, :name, :disc, :position, :duration, :genres) "
"ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, album_id = :album_id, name = :name, disc = :disc, position = :position, duration = :duration, "
"genres = :genres");
query.bindValue(":id", this->uuid.toString());
query.bindValue(":artist_id", !this->artist_uuid.isNull() ? this->artist_uuid.toString() : NULL);
query.bindValue(":album_id", this->album_uuid.has_value() ? this->album_uuid.value().toString() : NULL);
query.bindValue(":name", this->title);
query.bindValue(":disc", this->disc_number);
query.bindValue(":position", this->track_number);
query.bindValue(":duration", this->duration);
query.bindValue(":genres", this->genres.join(", "));
query.exec();
}
QString KotoTrack::getAlbumUuid() {
if (!this->album_uuid.has_value()) return this->album_uuid.value().toString();
return {};
}
QUuid KotoTrack::getArtistUuid() {
return this->artist_uuid;
}
int KotoTrack::getDiscNumber() {
return this->disc_number;
}
int KotoTrack::getDuration() {
return this->duration;
}
QList<QString> KotoTrack::getGenres() {
return QList {this->genres};
}
QString KotoTrack::getLyrics() {
return QString {this->lyrics};
}
QString KotoTrack::getNarrator() {
return QString {this->narrator};
}
QString KotoTrack::getPath() {
return QString {this->path};
}
QString KotoTrack::getTitle() {
return QString {this->title};
}
int KotoTrack::getTrackNumber() {
return this->track_number;
}
QUuid KotoTrack::getUuid() {
return this->uuid;
}
int KotoTrack::getYear() {
return this->year;
}
void KotoTrack::setAlbum(KotoAlbum* album) {
this->album_uuid = QUuid(album->uuid);
if (this->artist_uuid.isNull()) QUuid(album->artist_uuid);
}
void KotoTrack::setArtist(KotoArtist* artist) {
this->artist_uuid = QUuid(artist->uuid);
}
void KotoTrack::setDiscNumber(int num) {
this->disc_number = num;
}
void KotoTrack::setDuration(int num) {
this->duration = num;
}
void KotoTrack::setGenres(QList<QString> list) {
this->genres = QList {list};
}
void KotoTrack::setLyrics(QString str) {
this->lyrics = QString {str};
}
void KotoTrack::setNarrator(QString str) {
this->narrator = QString {str};
}
void KotoTrack::setPath(QString str) {
this->path = QString {str};
}
void KotoTrack::setTitle(QString str) {
this->title = QString {str};
}
void KotoTrack::setTrackNumber(int num) {
this->track_number = num;
}
void KotoTrack::setYear(int num) {
this->year = num;
}

View file

@ -0,0 +1,11 @@
["preferences.ui"]
album_info_show_description = true
album_info_show_genre = true
album_info_show_narrator = true
album_info_show_year = true
last_used_volume = 0.5
[[libraries]]
name = "Music"
path = "/home/joshua/Music"
type = "music"

17240
desktop/includes/toml.hpp Normal file

File diff suppressed because it is too large Load diff

51
desktop/main.cpp Normal file
View file

@ -0,0 +1,51 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>
#include <iostream>
#include <thread>
#include "config/config.hpp"
#include "datalake/database.hpp"
#include "datalake/indexer.hpp"
int main(int argc, char* argv[]) {
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
QGuiApplication app(argc, argv);
app.setApplicationDisplayName("Koto");
app.setDesktopFileName("com.github.joshstrobl.koto.desktop");
QQmlApplicationEngine engine;
engine.loadFromModule("com.github.joshstrobl.koto", "Main");
if (engine.rootObjects().isEmpty()) { return -1; }
std::thread([]() {
KotoConfig::create();
KotoDatabase::create();
KotoDatabase::instance().connect();
// If we needed to bootstrap, index all libraries, otherwise load the database
if (KotoDatabase::instance().requiredBootstrap()) {
indexAllLibraries();
} else {
KotoDatabase::instance().load();
std::cout << "===== Summary =====" << std::endl;
for (auto artist : Cartographer::instance().getArtists()) {
std::cout << "Artist: " << artist->getName().toStdString() << std::endl;
for (auto album : artist->getAlbums()) {
std::cout << " Album: " << album->getTitle().toStdString() << std::endl;
for (auto track : album->getTracks()) { std::cout << " Track: " << track->getTitle().toStdString() << std::endl; }
}
}
std::cout << "===== Tracks without albums and/or artists =====" << std::endl;
for (auto track : Cartographer::instance().getTracks()) {
if (track->album_uuid.has_value()) continue;
std::cout << "Track: " << track->getTitle().toStdString() << std::endl;
}
}
}).detach();
return app.exec();
}

24
desktop/qml/HomePage.qml Normal file
View file

@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import com.github.joshstrobl.koto
Kirigami.ScrollablePage {
Component {
id: listDelegate
Controls.ItemDelegate {
required property string name
text: name
width: ListView.view.width
}
}
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
delegate: listDelegate
model: Cartographer.artists
}
}

20
desktop/qml/Main.qml Normal file
View file

@ -0,0 +1,20 @@
import org.kde.kirigami as Kirigami
Kirigami.ApplicationWindow {
id: root
height: 600
title: "Koto"
visible: true
width: 1000
footer: PlayerBar {
}
globalDrawer: PrimaryNavigation {
windowRef: root
}
// TODO: Implement an onboarding page
pageStack.initialPage: Root {
}
}

View file

@ -0,0 +1,74 @@
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
ColumnLayout {
id: playerBar
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
spacing: 4
RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Controls.Slider {
id: seekSlider
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Layout.maximumWidth: parent.width - 2 * Kirigami.Units.largeSpacing
Layout.minimumWidth: parent.width - 2 * Kirigami.Units.largeSpacing
RowLayout {
anchors.left: parent.left
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "media-seek-backward"
}
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.medium
icon.name: "media-playback-start"
icon.width: Kirigami.Units.iconSizes.medium
}
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "media-seek-forward"
}
}
RowLayout {
anchors.right: parent.right
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "media-playlist-repeat"
}
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "media-playlist-shuffle"
}
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "playlist-symbolic"
}
Controls.Button {
flat: true
icon.height: Kirigami.Units.iconSizes.small
icon.name: "audio-volume-medium"
}
}
}
}

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
Kirigami.GlobalDrawer {
id: primaryNavigation
property Kirigami.ApplicationWindow windowRef
function isMobile(width) {
return width < 800;
}
function onWindowSizeChanged(width) {
const mobile = isMobile(width);
drawerOpen = !mobile;
modal = mobile;
height = mobile ? windowRef.height : windowRef.height - windowRef.footer.height;
}
collapseButtonVisible: false
drawerOpen: !isMobile()
edge: Qt.LeftEdge
height: parent.height - windowRef.footer.height
modal: false
actions: [
Kirigami.Action {
icon.name: "go-home"
text: "Home"
onTriggered: console.log("Home triggered")
},
Kirigami.Action {
expandible: true
icon.name: "bookmark"
text: "Audiobooks"
onTriggered: console.log("Audiobooks triggered")
},
Kirigami.Action {
expandible: true
icon.name: "emblem-music-symbolic"
text: "Music"
children: [
Kirigami.Action {
text: "Local Library"
onTriggered: console.log("Music Local Library triggered")
},
Kirigami.Action {
text: "Radio"
onTriggered: console.log("Music Radio triggered")
}
]
},
Kirigami.Action {
expandible: true
icon.name: "application-rss+xml-symbolic"
text: "Podcasts"
children: [
Kirigami.Action {
text: "Library"
onTriggered: console.log("Podcasts Library triggered")
},
Kirigami.Action {
text: "Find new podcasts"
onTriggered: console.log("Podcasts Find new podcasts triggered")
}
]
},
Kirigami.Action {
expandible: true
icon.name: "music-playlist-symbolic"
text: "Playlists"
children: [
Kirigami.Action {
text: "Library"
onTriggered: console.log("Playlists Library triggered")
}
]
// TODO: Generate list of playlists
}
]
header: Kirigami.SearchField {
id: searchEntry
Layout.topMargin: Kirigami.Units.largeSpacing
placeholderText: qsTr("Search")
}
Component.onCompleted: {
if (Kirigami.Settings.isMobile)
return;
if (windowRef)
windowRef.onWidthChanged.connect(() => onWindowSizeChanged(windowRef.width));
}
}

24
desktop/qml/Root.qml Normal file
View file

@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
Kirigami.Page {
id: rootPage
ColumnLayout {
id: rootLayout
anchors.fill: parent
Controls.StackView {
id: rootStack
Layout.fillHeight: true
Layout.fillWidth: true
initialItem: HomePage {
}
}
}
}

2625
jsc.cfg

File diff suppressed because it is too large Load diff

View file

@ -1,35 +0,0 @@
project('koto', 'c',
version: '0.1.0',
meson_version: '>= 0.57.0',
default_options: [
'c_std=gnu11',
'warning_level=2',
'werror=true',
],
)
i18n = import('i18n')
gnome = import('gnome')
config_h = configuration_data()
config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
config_h.set_quoted('GETTEXT_PACKAGE', 'koto')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
configure_file(
output: 'koto-config.h',
configuration: config_h,
)
c = meson.get_compiler('c')
toml_dep = c.find_library('toml', required: true)
subdir('theme')
subdir('data')
subdir('src')
subdir('po')
gnome.post_install(
glib_compile_schemas: true,
gtk_update_icon_cache: false
)
meson.add_install_script('build-aux/meson/postinstall.py')

View file

View file

@ -1,7 +0,0 @@
data/com.github.joshstrobl.koto.desktop.in
data/com.github.joshstrobl.koto.appdata.xml.in
data/com.github.joshstrobl.koto.gschema.xml
src/koto-window.ui
src/main.c
src/koto-window.c

View file

@ -1 +0,0 @@
i18n.gettext('koto', preset: 'glib')

View file

@ -1,423 +0,0 @@
/* action-bar.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../config/config.h"
#include "../db/cartographer.h"
#include "../indexer/album-playlist-funcs.h"
#include "../pages/music/music-local.h"
#include "../playlist/add-remove-track-popover.h"
#include "../playlist/current.h"
#include "../playback/engine.h"
#include "../koto-utils.h"
#include "../koto-window.h"
#include "action-bar.h"
#include "button.h"
extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup;
extern KotoCartographer * koto_maps;
extern KotoConfig * config;
extern KotoCurrentPlaylist * current_playlist;
extern KotoPageMusicLocal * music_local_page;
extern KotoPlaybackEngine * playback_engine;
extern KotoWindow * main_window;
enum {
SIGNAL_CLOSED,
N_SIGNALS
};
static guint actionbar_signals[N_SIGNALS] = {
0
};
struct _KotoActionBar {
GObject parent_instance;
GtkActionBar * main;
GtkWidget * center_box_content;
GtkWidget * start_box_content;
GtkWidget * stop_box_content;
KotoButton * close_button;
GtkWidget * go_to_artist;
GtkWidget * playlists;
GtkWidget * play_track;
GtkWidget * remove_from_playlist;
GList * current_list;
gchar * current_album_uuid;
gchar * current_playlist_uuid;
KotoActionBarRelative relative;
};
struct _KotoActionBarClass {
GObjectClass parent_class;
void (* closed) (KotoActionBar * self);
};
G_DEFINE_TYPE(KotoActionBar, koto_action_bar, G_TYPE_OBJECT);
KotoActionBar * action_bar;
static void koto_action_bar_class_init(KotoActionBarClass * c) {
GObjectClass * gobject_class = G_OBJECT_CLASS(c);
actionbar_signals[SIGNAL_CLOSED] = g_signal_new(
"closed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoActionBarClass, closed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
}
static void koto_action_bar_init(KotoActionBar * self) {
self->main = GTK_ACTION_BAR(gtk_action_bar_new()); // Create a new action bar
self->current_list = NULL;
self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
self->go_to_artist = gtk_button_new_with_label("Go to Artist");
self->playlists = gtk_button_new_with_label("Playlists");
self->play_track = gtk_button_new_with_label("Play");
self->remove_from_playlist = gtk_button_new_with_label("Remove from Playlist");
gtk_widget_add_css_class(self->playlists, "suggested-action");
gtk_widget_add_css_class(self->play_track, "suggested-action");
gtk_widget_add_css_class(self->remove_from_playlist, "destructive-action");
gtk_widget_set_size_request(self->play_track, 160, -1);
self->center_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
self->start_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
self->stop_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20);
gtk_box_prepend(GTK_BOX(self->start_box_content), GTK_WIDGET(self->go_to_artist));
gtk_box_prepend(GTK_BOX(self->center_box_content), GTK_WIDGET(self->play_track));
gtk_box_prepend(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->playlists));
gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->remove_from_playlist));
gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->close_button));
gtk_action_bar_pack_start(self->main, self->start_box_content);
gtk_action_bar_pack_end(self->main, self->stop_box_content);
gtk_action_bar_set_center_widget(self->main, self->center_box_content);
// Hide all the widgets by default
gtk_widget_hide(GTK_WIDGET(self->go_to_artist));
gtk_widget_hide(GTK_WIDGET(self->playlists));
gtk_widget_hide(GTK_WIDGET(self->play_track));
gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist));
// Set up bindings
koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_action_bar_handle_close_button_clicked), self);
g_signal_connect(self->go_to_artist, "clicked", G_CALLBACK(koto_action_bar_handle_go_to_artist_button_clicked), self);
g_signal_connect(self->playlists, "clicked", G_CALLBACK(koto_action_bar_handle_playlists_button_clicked), self);
g_signal_connect(self->play_track, "clicked", G_CALLBACK(koto_action_bar_handle_play_track_button_clicked), self);
g_signal_connect(self->remove_from_playlist, "clicked", G_CALLBACK(koto_action_bar_handle_remove_from_playlist_button_clicked), self);
koto_action_bar_toggle_reveal(self, FALSE); // Hide by default
}
void koto_action_bar_close(KotoActionBar * self) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
gtk_widget_hide(GTK_WIDGET(koto_add_remove_track_popup)); // Hide the Add / Remove Track Playlists popover
koto_action_bar_toggle_reveal(self, FALSE); // Hide the action bar
g_signal_emit(
self,
actionbar_signals[SIGNAL_CLOSED],
0,
NULL
);
}
GtkActionBar * koto_action_bar_get_main(KotoActionBar * self) {
if (!KOTO_IS_ACTION_BAR(self)) {
return NULL;
}
return self->main;
}
void koto_action_bar_handle_close_button_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
koto_action_bar_close(data);
}
void koto_action_bar_handle_go_to_artist_button_clicked(
GtkButton * button,
gpointer data
) {
(void) button;
KotoActionBar * self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly one item
return;
}
KotoTrack * selected_track = g_list_nth_data(self->current_list, 0); // Get the first item
if (!KOTO_IS_TRACK(selected_track)) { // Not a track
return;
}
gchar * artist_uuid = NULL;
g_object_get(
selected_track,
"artist-uuid",
&artist_uuid,
NULL
);
koto_page_music_local_go_to_artist_by_uuid(music_local_page, artist_uuid); // Go to the artist
koto_window_go_to_page(main_window, "music.local"); // Navigate to the local music stack so we can see the substack page
koto_action_bar_close(self); // Close the action bar
}
void koto_action_bar_handle_playlists_button_clicked(
GtkButton * button,
gpointer data
) {
(void) button;
KotoActionBar * self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content
return;
}
koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the popover tracks
koto_add_remove_track_popover_set_pointing_to_widget(koto_add_remove_track_popup, GTK_WIDGET(self->playlists), GTK_POS_TOP); // Show the popover above the button
gtk_widget_show(GTK_WIDGET(koto_add_remove_track_popup));
}
void koto_action_bar_handle_play_track_button_clicked(
GtkButton * button,
gpointer data
) {
(void) button;
KotoActionBar * self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly 1 item selected
goto doclose;
}
KotoTrack * track = g_list_nth_data(self->current_list, 0); // Get the first track
if (!KOTO_IS_TRACK(track)) { // Not a track
goto doclose;
}
KotoPlaylist * playlist = NULL;
if (self->relative == KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE) { // Relative to a playlist
playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->current_playlist_uuid);
} else if (self->relative == KOTO_ACTION_BAR_IS_ALBUM_RELATIVE) { // Relative to an Album
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->current_album_uuid); // Get the Album
if (KOTO_IS_ALBUM(album)) { // Have an Album
playlist = koto_album_get_playlist(album); // Create our playlist dynamically for the Album
}
}
if (KOTO_IS_PLAYLIST(playlist)) { // Is a playlist
koto_current_playlist_set_playlist(current_playlist, playlist, FALSE, FALSE); // Update our playlist to the one associated with the track we are playing
koto_playlist_set_track_as_current(playlist, koto_track_get_uuid(track)); // Get this track as the current track in the position
}
gboolean continue_playback = FALSE;
g_object_get(config, "playback-continue-on-playlist", &continue_playback, NULL);
koto_playback_engine_set_track_by_uuid(playback_engine, koto_track_get_uuid(track), continue_playback); // Set the track to play
doclose:
koto_action_bar_close(self);
}
void koto_action_bar_handle_remove_from_playlist_button_clicked(
GtkButton * button,
gpointer data
) {
(void) button;
KotoActionBar * self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content
goto doclose;
}
if (!koto_utils_string_is_valid(self->current_playlist_uuid)) { // Not valid UUID
goto doclose;
}
KotoPlaylist * playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->current_playlist_uuid);
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
goto doclose;
}
GList * cur_list;
for (cur_list = self->current_list; cur_list != NULL; cur_list = cur_list->next) { // For each KotoTrack
KotoTrack * track = cur_list->data;
koto_playlist_remove_track_by_uuid(playlist, koto_track_get_uuid(track)); // Remove this track
}
doclose:
koto_action_bar_close(self);
}
void koto_action_bar_set_tracks_in_album_selection(
KotoActionBar * self,
gchar * album_uuid,
GList * tracks
) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set
g_free(self->current_album_uuid);
}
if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // Playlist UUID currently set
g_free(self->current_playlist_uuid);
}
self->current_playlist_uuid = NULL;
self->current_album_uuid = g_strdup(album_uuid);
self->relative = KOTO_ACTION_BAR_IS_ALBUM_RELATIVE;
g_list_free(self->current_list);
self->current_list = g_list_copy(tracks);
koto_add_remove_track_popover_clear_tracks(koto_add_remove_track_popup); // Clear the current popover contents
koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the associated tracks to remove
koto_action_bar_toggle_go_to_artist_visibility(self, FALSE);
koto_action_bar_toggle_play_button_visibility(self, g_list_length(self->current_list) == 1);
gtk_widget_show(GTK_WIDGET(self->playlists));
gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist));
}
void koto_action_bar_set_tracks_in_playlist_selection(
KotoActionBar * self,
gchar * playlist_uuid,
GList * tracks
) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set
g_free(self->current_album_uuid);
}
if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // Playlist UUID currently set
g_free(self->current_playlist_uuid);
}
self->current_album_uuid = NULL;
self->current_playlist_uuid = g_strdup(playlist_uuid);
self->relative = KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE;
g_list_free(self->current_list);
self->current_list = g_list_copy(tracks);
gboolean single_selected = g_list_length(tracks) == 1;
koto_action_bar_toggle_go_to_artist_visibility(self, single_selected);
koto_action_bar_toggle_play_button_visibility(self, single_selected);
gtk_widget_hide(GTK_WIDGET(self->playlists));
gtk_widget_show(GTK_WIDGET(self->remove_from_playlist));
}
void koto_action_bar_toggle_go_to_artist_visibility(
KotoActionBar * self,
gboolean visible
) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
(visible) ? gtk_widget_show(GTK_WIDGET(self->go_to_artist)) : gtk_widget_hide(GTK_WIDGET(self->go_to_artist));
}
void koto_action_bar_toggle_play_button_visibility(
KotoActionBar * self,
gboolean visible
) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
(visible) ? gtk_widget_show(GTK_WIDGET(self->play_track)) : gtk_widget_hide(GTK_WIDGET(self->play_track));
}
void koto_action_bar_toggle_reveal(
KotoActionBar * self,
gboolean state
) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
gtk_action_bar_set_revealed(self->main, state);
}
KotoActionBar * koto_action_bar_new() {
return g_object_new(
KOTO_TYPE_ACTION_BAR,
NULL
);
}

View file

@ -1,105 +0,0 @@
/* action-bar.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../indexer/structs.h"
G_BEGIN_DECLS
typedef enum {
KOTO_ACTION_BAR_IS_ALBUM_RELATIVE = 1,
KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE = 2
} KotoActionBarRelative;
#define KOTO_TYPE_ACTION_BAR (koto_action_bar_get_type())
#define KOTO_IS_ACTION_BAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ACTION_BAR))
typedef struct _KotoActionBar KotoActionBar;
typedef struct _KotoActionBarClass KotoActionBarClass;
GLIB_AVAILABLE_IN_ALL
GType koto_action_bar_get_type(void) G_GNUC_CONST;
/**
* Action Bar Functions
**/
KotoActionBar * koto_action_bar_new(void);
void koto_action_bar_close(KotoActionBar * self);
GtkActionBar * koto_action_bar_get_main(KotoActionBar * self);
void koto_action_bar_handle_close_button_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer data
);
void koto_action_bar_handle_go_to_artist_button_clicked(
GtkButton * button,
gpointer data
);
void koto_action_bar_handle_playlists_button_clicked(
GtkButton * button,
gpointer data
);
void koto_action_bar_handle_play_track_button_clicked(
GtkButton * button,
gpointer data
);
void koto_action_bar_handle_remove_from_playlist_button_clicked(
GtkButton * button,
gpointer data
);
void koto_action_bar_set_tracks_in_album_selection(
KotoActionBar * self,
gchar * album_uuid,
GList * tracks
);
void koto_action_bar_set_tracks_in_playlist_selection(
KotoActionBar * self,
gchar * playlist_uuid,
GList * tracks
);
void koto_action_bar_toggle_go_to_artist_visibility(
KotoActionBar * self,
gboolean visible
);
void koto_action_bar_toggle_play_button_visibility(
KotoActionBar * self,
gboolean visible
);
void koto_action_bar_toggle_reveal(
KotoActionBar * self,
gboolean state
);
G_END_DECLS

View file

@ -1,289 +0,0 @@
/* album-info.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../config/config.h"
#include "../db/cartographer.h"
#include "../indexer/structs.h"
#include "../koto-utils.h"
#include "album-info.h"
extern KotoCartographer * koto_maps;
extern KotoConfig * config;
enum {
PROP_0,
PROP_TYPE,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
struct _KotoAlbumInfo {
GtkBox parent_instance;
KotoAlbumInfoType type;
KotoAlbum * album;
GtkWidget * name_year_box;
GtkWidget * genres_tags_list;
GtkWidget * name_label;
GtkWidget * year_badge;
GtkWidget * narrator_label;
GtkWidget * description_label;
};
struct _KotoAlbumInfoClass {
GtkBoxClass parent_class;
};
G_DEFINE_TYPE(KotoAlbumInfo, koto_album_info, GTK_TYPE_BOX);
static void koto_album_info_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_album_info_class_init(KotoAlbumInfoClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_album_info_set_property;
props[PROP_TYPE] = g_param_spec_string(
"type",
"Type of AlbumInfo component",
"Type of AlbumInfo component",
"album",
G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_album_info_init(KotoAlbumInfo * self) {
gtk_widget_add_css_class(GTK_WIDGET(self), "album-info");
self->type = KOTO_ALBUM_INFO_TYPE_ALBUM; // Default to Album here
self->name_year_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create out name box for the name and year
gtk_widget_add_css_class(self->name_year_box, "album-title-year-combo");
self->name_label = gtk_label_new(NULL); // Create our name label
gtk_widget_set_halign(self->name_label, GTK_ALIGN_START);
gtk_widget_set_valign(self->name_label, GTK_ALIGN_START);
gtk_widget_add_css_class(self->name_label, "album-title");
self->year_badge = gtk_label_new(NULL); // Create our year badge
gtk_widget_add_css_class(self->year_badge, "album-year");
gtk_widget_add_css_class(self->year_badge, "label-badge");
gtk_widget_set_valign(self->year_badge, GTK_ALIGN_CENTER); // Center vertically align the year badge
self->narrator_label = gtk_label_new(NULL); // Create our narrator label
gtk_widget_add_css_class(self->narrator_label, "album-narrator");
gtk_widget_set_halign(self->narrator_label, GTK_ALIGN_START);
self->description_label = gtk_label_new(NULL); // Create our description label
gtk_widget_add_css_class(self->description_label, "album-description");
gtk_widget_set_halign(self->description_label, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(self->name_year_box), self->name_label); // Add the name label to the name + year box
gtk_box_append(GTK_BOX(self->name_year_box), self->year_badge); // Add the year badge to the name + year box
gtk_box_append(GTK_BOX(self), self->name_year_box);
gtk_box_append(GTK_BOX(self), self->narrator_label);
gtk_box_append(GTK_BOX(self), self->description_label);
g_signal_connect(config, "notify::ui-info-show-description", G_CALLBACK(koto_album_info_apply_configuration_state), self);
g_signal_connect(config, "notify::ui-info-show-genres", G_CALLBACK(koto_album_info_apply_configuration_state), self);
g_signal_connect(config, "notify::ui-info-show-narrator", G_CALLBACK(koto_album_info_apply_configuration_state), self);
g_signal_connect(config, "notify::ui-info-show-year", G_CALLBACK(koto_album_info_apply_configuration_state), self);
}
static void koto_album_info_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoAlbumInfo * self = KOTO_ALBUM_INFO(obj);
switch (prop_id) {
case PROP_TYPE:
koto_album_info_set_type(self, g_value_get_string(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_album_info_apply_configuration_state(
KotoConfig * c,
guint prop_id,
KotoAlbumInfo * self
) {
(void) c;
(void) prop_id;
gboolean show_description = TRUE;
gboolean show_genres = TRUE;
gboolean show_narrator = TRUE;
gboolean show_year = TRUE;
g_object_get(
config,
"ui-album-info-show-description",
&show_description,
"ui-album-info-show-genres",
&show_genres,
"ui-album-info-show-narrator",
&show_narrator,
"ui-album-info-show-year",
&show_year,
NULL
);
if (self->type != KOTO_ALBUM_INFO_TYPE_AUDIOBOOK) { // If the type is NOT an audiobook
show_narrator = FALSE; // Narrator should never be shown. Only really applicable to audiobook
}
if (self->type == KOTO_ALBUM_INFO_TYPE_PODCAST) { // If the type is podcast
show_year = FALSE; // Year isn't really applicable to podcast, at least not the "global" year
}
if (koto_utils_string_is_valid(koto_album_get_description(self->album)) && show_description) { // Have description to show in the first place, and should show it
gtk_widget_show(self->description_label);
} else { // Don't have content, just hide it
gtk_widget_hide(self->description_label);
}
if (!KOTO_IS_ALBUM(self->album)) { // Don't have an album defined
return;
}
if (GTK_IS_FLOW_BOX(self->genres_tags_list)) {
if ((g_list_length(koto_album_get_genres(self->album)) > 0) && show_genres) { // Have genres to show in the first place, and should show it
gtk_widget_show(self->genres_tags_list);
} else {
gtk_widget_hide(self->genres_tags_list);
}
}
if (koto_utils_string_is_valid(koto_album_get_narrator(self->album)) && show_narrator) { // Have narrator and should show it
gtk_widget_show(self->narrator_label);
} else {
gtk_widget_hide(self->narrator_label);
}
if ((koto_album_get_year(self->album) != 0) && show_year) { // Have year and should show it
gtk_widget_show(self->year_badge);
} else {
gtk_widget_hide(self->year_badge);
}
}
void koto_album_info_set_album_uuid(
KotoAlbumInfo * self,
gchar * album_uuid
) {
if (!KOTO_IS_ALBUM_INFO(self)) {
return;
}
if (!koto_utils_string_is_valid(album_uuid)) {
return;
}
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Get the album
if (!KOTO_IS_ALBUM(album)) { // Is not an album
return;
}
self->album = album;
gtk_label_set_text(GTK_LABEL(self->name_label), koto_album_get_name(self->album)); // Set the name label text to the album
gtk_label_set_text(GTK_LABEL(self->year_badge), g_strdup_printf("%li", koto_album_get_year(self->album))); // Set the year label text to any year for the album
gchar * narrator = koto_album_get_narrator(self->album);
if (koto_utils_string_is_valid(narrator)) { // Have a narrator
gtk_label_set_text(GTK_LABEL(self->narrator_label), g_strdup_printf("Narrated by %s", koto_album_get_narrator(self->album))); // Set narrated by string
}
gchar * description = koto_album_get_description(self->album);
if (koto_utils_string_is_valid(description)) { // Have a description
gtk_label_set_markup(GTK_LABEL(self->description_label), description); // Set the markup so we treat it more like HTML and let pango do its thing
}
if (GTK_IS_BOX(self->genres_tags_list)) { // Genres Flow
gtk_box_remove(GTK_BOX(self), self->genres_tags_list); // Remove the genres flow from the box
}
self->genres_tags_list = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create our flow box for the genres
gtk_widget_add_css_class(self->genres_tags_list, "genres-tag-list");
GList * album_genres = koto_album_get_genres(self->album);
if (g_list_length(album_genres) != 0) { // Have genres
GList * cur_list;
for (cur_list = album_genres; cur_list != NULL; cur_list = cur_list->next) { // Iterate over each genre
GtkWidget * genre_label = gtk_label_new(koto_utils_string_title(cur_list->data));
gtk_widget_add_css_class(genre_label, "label-badge"); // Add label badge styling
gtk_box_append(GTK_BOX(self->genres_tags_list), genre_label); // Append to the genre tags list
}
} else {
gtk_widget_hide(self->genres_tags_list); // Hide the genres tag list
}
gtk_box_insert_child_after(GTK_BOX(self), self->genres_tags_list, self->name_year_box); // Add after the name+year flowbox
koto_album_info_apply_configuration_state(NULL, 0, self); // Apply our configuration state immediately so we know what to show and hide for this album based on the config
}
void koto_album_info_set_type(
KotoAlbumInfo * self,
const gchar * type
) {
if (!KOTO_IS_ALBUM_INFO(self)) {
return;
}
if (g_strcmp0(type, "audiobook") == 0) { // If this is an audiobook
self->type = KOTO_ALBUM_INFO_TYPE_AUDIOBOOK;
} else if (g_strcmp0(type, "podcast") == 0) { // If this is a podcast
self->type = KOTO_ALBUM_INFO_TYPE_PODCAST;
} else {
self->type = KOTO_ALBUM_INFO_TYPE_ALBUM;
}
}
KotoAlbumInfo * koto_album_info_new(gchar * type) {
return g_object_new(
KOTO_TYPE_ALBUM_INFO,
"orientation",
GTK_ORIENTATION_VERTICAL,
"type",
type,
NULL
);
}

View file

@ -1,52 +0,0 @@
/* album-info.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../config/config.h"
G_BEGIN_DECLS
typedef enum {
KOTO_ALBUM_INFO_TYPE_ALBUM,
KOTO_ALBUM_INFO_TYPE_AUDIOBOOK,
KOTO_ALBUM_INFO_TYPE_PODCAST
} KotoAlbumInfoType;
#define KOTO_TYPE_ALBUM_INFO (koto_album_info_get_type())
G_DECLARE_FINAL_TYPE(KotoAlbumInfo, koto_album_info, KOTO, ALBUM_INFO, GtkBox);
#define KOTO_IS_ALBUM_INFO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM_INFO))
void koto_album_info_apply_configuration_state(
KotoConfig * c,
guint prop_id,
KotoAlbumInfo * self
);
void koto_album_info_set_album_uuid(
KotoAlbumInfo * self,
gchar * album_uuid
);
void koto_album_info_set_type(
KotoAlbumInfo * self,
const gchar * type
);
KotoAlbumInfo * koto_album_info_new(gchar * type);

View file

@ -1,818 +0,0 @@
/* koto-button.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtk-4.0/gtk/gtk.h>
#include "config/config.h"
#include "button.h"
#include "../koto-window.h"
#include "../koto-utils.h"
extern KotoWindow * main_window;
struct _PixbufSize {
guint size;
};
typedef struct _PixbufSize PixbufSize;
static PixbufSize * pixbuf_sizes = NULL;
static guint pixbuf_sizes_allocated = 0;
static void init_pixbuf_sizes() {
if (pixbuf_sizes == NULL) {
pixbuf_sizes = g_new(PixbufSize, NUM_BUILTIN_SIZES);
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_INVALID].size = 0;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_TINY].size = 16;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_SMALL].size = 24;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_NORMAL].size = 32;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_LARGE].size = 64;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_MASSIVE].size = 96;
pixbuf_sizes[KOTO_BUTTON_PIXBUF_SIZE_GODLIKE].size = 128;
pixbuf_sizes_allocated = NUM_BUILTIN_SIZES;
}
}
guint koto_get_pixbuf_size(KotoButtonPixbufSize s) {
init_pixbuf_sizes();
return pixbuf_sizes[s].size;
}
enum {
PROP_BTN_0,
PROP_PIX_SIZE,
PROP_TEXT,
PROP_BADGE_TEXT,
PROP_USE_FROM_FILE,
PROP_IMAGE_FILE_PATH,
PROP_ICON_NAME,
PROP_ALT_ICON_NAME,
PROP_RESOURCE_PATH,
N_BTN_PROPERTIES
};
static GParamSpec * btn_props[N_BTN_PROPERTIES] = {
NULL,
};
struct _KotoButton {
GtkBox parent_instance;
guint pix_size;
gpointer arbitrary_data;
GtkWidget * button_pic;
GtkWidget * badge_label;
GtkWidget * button_label;
GtkGesture * left_click_gesture;
GtkGesture * right_click_gesture;
gchar * image_file_path;
gchar * badge_text;
gchar * icon_name;
gchar * alt_icon_name;
gchar * resource_path;
gchar * text;
KotoButtonImagePosition image_position;
gboolean use_from_file;
gboolean currently_showing_alt;
};
struct _KotoButtonClass {
GtkBoxClass parent_class;
};
G_DEFINE_TYPE(KotoButton, koto_button, GTK_TYPE_BOX);
static void koto_button_constructed(GObject * obj);
static void koto_button_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_button_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_button_class_init(KotoButtonClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->constructed = koto_button_constructed;
gobject_class->set_property = koto_button_set_property;
gobject_class->get_property = koto_button_get_property;
btn_props[PROP_PIX_SIZE] = g_param_spec_uint(
"pixbuf-size",
"Pixbuf Size",
"Size of the pixbuf",
koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_TINY),
koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_GODLIKE),
koto_get_pixbuf_size(KOTO_BUTTON_PIXBUF_SIZE_SMALL),
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_TEXT] = g_param_spec_string(
"button-text",
"Button Text",
"Text of Button",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_BADGE_TEXT] = g_param_spec_string(
"badge-text",
"Badge Text",
"Text of Badge",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_USE_FROM_FILE] = g_param_spec_boolean(
"use-from-file",
"Use from a file / file name",
"Use from a file / file name",
FALSE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_IMAGE_FILE_PATH] = g_param_spec_string(
"image-file-path",
"File path to image",
"File path to image",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_ICON_NAME] = g_param_spec_string(
"icon-name",
"Icon Name",
"Name of Icon",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_ALT_ICON_NAME] = g_param_spec_string(
"alt-icon-name",
"Name of an Alternate Icon",
"Name of an Alternate Icon",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
btn_props[PROP_RESOURCE_PATH] = g_param_spec_string(
"resource-path",
"Resource Path to an Icon",
"Resource Path to an Icon",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_BTN_PROPERTIES, btn_props);
}
static void koto_button_init(KotoButton * self) {
self->currently_showing_alt = FALSE;
self->image_position = KOTO_BUTTON_IMAGE_POS_LEFT;
self->left_click_gesture = gtk_gesture_click_new(); // Set up our left click gesture
self->right_click_gesture = gtk_gesture_click_new(); // Set up our right click gesture
self->resource_path = NULL;
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->left_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_PRIMARY); // Only allow left clicks on left click gesture
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->right_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_SECONDARY); // Only allow right clicks on right click gesture
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->left_click_gesture)); // Add our left click gesture
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->right_click_gesture)); // Add our right click gesture
GtkEventController * motion = gtk_event_controller_motion_new(); // Create a new motion controller
g_signal_connect(motion, "enter", G_CALLBACK(koto_button_handle_mouse_enter), self); // Handle mouse enter on motion
g_signal_connect(motion, "leave", G_CALLBACK(koto_button_handle_mouse_leave), self); // Handle mouse leave on motion
gtk_widget_add_controller(GTK_WIDGET(self), motion);
}
static void koto_button_constructed(GObject * obj) {
KotoButton * self = KOTO_BUTTON(obj);
GtkStyleContext * style = gtk_widget_get_style_context(GTK_WIDGET(self));
gtk_style_context_add_class(style, "koto-button");
G_OBJECT_CLASS(koto_button_parent_class)->constructed(obj);
}
static void koto_button_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoButton * self = KOTO_BUTTON(obj);
switch (prop_id) {
case PROP_IMAGE_FILE_PATH:
g_value_set_string(val, self->image_file_path);
break;
case PROP_USE_FROM_FILE:
g_value_set_boolean(val, self->use_from_file);
break;
case PROP_PIX_SIZE:
g_value_set_uint(val, self->pix_size);
break;
case PROP_TEXT:
g_value_set_string(val, self->text);
break;
case PROP_BADGE_TEXT:
g_value_set_string(val, self->badge_text);
break;
case PROP_ICON_NAME:
g_value_set_string(val, self->icon_name);
break;
case PROP_ALT_ICON_NAME:
g_value_set_string(val, self->alt_icon_name);
break;
case PROP_RESOURCE_PATH:
g_value_set_string(val, self->resource_path);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_button_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoButton * self = KOTO_BUTTON(obj);
switch (prop_id) {
case PROP_PIX_SIZE:
koto_button_set_pixbuf_size(self, g_value_get_uint(val));
break;
case PROP_TEXT:
if (koto_utils_string_is_valid(g_value_get_string(val))) {
koto_button_set_text(self, (gchar*) g_value_get_string(val));
}
break;
case PROP_BADGE_TEXT:
koto_button_set_badge_text(self, g_strdup(g_value_get_string(val)));
break;
case PROP_USE_FROM_FILE:
self->use_from_file = g_value_get_boolean(val);
break;
case PROP_IMAGE_FILE_PATH:
koto_button_set_file_path(self, (gchar*) g_value_get_string(val));
break;
case PROP_ICON_NAME:
koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), FALSE);
if (!self->currently_showing_alt) { // Not showing alt
koto_button_show_image(self, FALSE);
}
break;
case PROP_ALT_ICON_NAME:
koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), TRUE);
if (self->currently_showing_alt) { // Currently showing the alt image
koto_button_show_image(self, TRUE);
}
break;
case PROP_RESOURCE_PATH:
koto_button_set_resource_path(self, g_strdup(g_value_get_string(val)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_button_add_click_handler(
KotoButton * self,
KotoButtonClickType button,
GCallback handler,
gpointer user_data
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if ((button != KOTO_BUTTON_CLICK_TYPE_PRIMARY) && (button != KOTO_BUTTON_CLICK_TYPE_SECONDARY)) { // Not valid type
return;
}
g_signal_connect((button == KOTO_BUTTON_CLICK_TYPE_PRIMARY) ? self->left_click_gesture : self->right_click_gesture, "pressed", handler, user_data);
}
void koto_button_flip(KotoButton * self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
koto_button_show_image(self, !self->currently_showing_alt);
}
gpointer koto_button_get_data(KotoButton * self) {
return self->arbitrary_data;
}
void koto_button_global_page_nav_callback(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoButton * btn = KOTO_BUTTON(user_data); // Cast our user data as a button
if (!KOTO_IS_BUTTON(btn)) { // Not a button
return;
}
gchar * btn_nav_uuid = (gchar*) koto_button_get_data(btn); // Get the data
if (!koto_utils_string_is_valid(btn_nav_uuid)) { // Not a valid string
return;
}
koto_window_go_to_page(main_window, btn_nav_uuid);
}
void koto_button_handle_mouse_enter(
GtkEventControllerMotion * controller,
double x,
double y,
gpointer user_data
) {
(void) controller;
(void) x;
(void) y;
KotoButton * self = user_data;
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
koto_button_set_pseudoactive_styling(self); // Set to be pseudoactive in styling
}
void koto_button_handle_mouse_leave(
GtkEventControllerMotion * controller,
gpointer user_data
) {
(void) controller;
KotoButton * self = user_data;
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
koto_button_unset_pseudoactive_styling(self); // Unset the pseudoactive styling
}
void koto_button_hide_image(KotoButton * self) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (GTK_IS_WIDGET(self->button_pic)) { // Is a widget
gtk_widget_hide(self->button_pic);
}
}
void koto_button_set_data(
KotoButton * self,
gpointer data
) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
self->arbitrary_data = data;
}
void koto_button_set_pseudoactive_styling(KotoButton * self) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (gtk_widget_has_css_class(GTK_WIDGET(self), "pseudoactive")) { // Already is pseudoactive
return;
}
gtk_widget_add_css_class(GTK_WIDGET(self), "pseudoactive"); // Set to pseudoactive
}
void koto_button_set_badge_text(
KotoButton * self,
gchar * text
) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (koto_utils_string_is_valid(self->badge_text)) { // Have existing text
g_free(self->badge_text);
}
if (!koto_utils_string_is_valid(text)) { // If the text is empty
self->badge_text = NULL;
if (GTK_IS_LABEL(self->badge_label)) { // If badge label already exists
gtk_widget_hide(self->badge_label); // Hide the label
}
return;
}
self->badge_text = g_strdup(text);
if (GTK_IS_LABEL(self->badge_label)) { // If badge label already exists
gtk_label_set_text(GTK_LABEL(self->badge_label), self->badge_text);
} else {
self->badge_label = gtk_label_new(self->badge_text); // Create our label
gtk_box_append(GTK_BOX(self), self->badge_label);
}
g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_BADGE_TEXT]);
}
void koto_button_set_file_path(
KotoButton * self,
gchar * file_path
) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (!koto_utils_string_is_valid(file_path)) { // file path is invalid
return;
}
if (g_strcmp0(self->image_file_path, file_path) == 0) { // Request setting as same file
return;
}
if (koto_utils_string_is_valid(self->image_file_path)) { // image file path is valid
g_free(self->image_file_path);
}
self->use_from_file = TRUE;
self->image_file_path = g_strdup(file_path);
koto_button_show_image(self, FALSE);
}
void koto_button_set_icon_name(
KotoButton * self,
gchar * icon_name,
gboolean for_alt
) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (!koto_utils_string_is_valid(icon_name)) { // Not a valid icon
return;
}
gchar * copied_icon_name = g_strdup(icon_name);
if (for_alt) { // Is for the alternate icon
if ((self->alt_icon_name != NULL) && strcmp(icon_name, self->alt_icon_name) != 0) { // If the icons are different
g_free(self->alt_icon_name);
}
self->alt_icon_name = copied_icon_name;
} else {
if ((self->icon_name != NULL) && strcmp(icon_name, self->icon_name) != 0) {
g_free(self->icon_name);
}
self->icon_name = copied_icon_name;
}
gboolean hide_image = FALSE;
if (for_alt && self->currently_showing_alt && ((self->alt_icon_name == NULL) || strcmp(self->alt_icon_name, "") == 0)) { // For alt, alt is currently showing, and no longer have alt
hide_image = TRUE;
} else if (!for_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Not for alt, no icon
hide_image = TRUE;
}
if (GTK_IS_IMAGE(self->button_pic)) { // If we already have a button image
if (hide_image) { // Should hide the image
gtk_widget_hide(self->button_pic); // Hide
} else { // Should be showing the image
gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), self->icon_name); // Just update the existing image immediately
}
}
g_object_notify_by_pspec(G_OBJECT(self), for_alt ? btn_props[PROP_ALT_ICON_NAME] : btn_props[PROP_ICON_NAME]);
}
void koto_button_set_image_position(
KotoButton * self,
KotoButtonImagePosition pos
) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (self->image_position == pos) { // Is a different position that currently
return;
}
if (GTK_IS_WIDGET(self->button_pic)) { // Button is already defined
if (pos == KOTO_BUTTON_IMAGE_POS_RIGHT) { // If we want to move the image to the right
gtk_box_reorder_child_after(GTK_BOX(self), self->button_pic, self->button_label); // Move image to after label
} else { // Moving image to left
gtk_box_reorder_child_after(GTK_BOX(self), self->button_label, self->button_pic); // Move label to after image
}
}
self->image_position = pos;
}
void koto_button_set_pixbuf_size(
KotoButton * self,
guint size
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (size == self->pix_size) {
return;
}
self->pix_size = size;
gtk_widget_set_size_request(GTK_WIDGET(self), self->pix_size, self->pix_size);
g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_PIX_SIZE]);
}
void koto_button_set_resource_path(
KotoButton * self,
gchar * resource_path
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (!koto_utils_string_is_valid(resource_path)) { // Not a valid string
return;
}
if (koto_utils_string_is_valid(self->resource_path)) { // Have a resource path already
g_free(self->resource_path); // Free it
}
self->resource_path = g_strdup(resource_path);
if (GTK_IS_IMAGE(self->button_pic)) { // Already have a button image
gtk_image_set_from_resource(GTK_IMAGE(self->button_pic), self->resource_path);
} else {
self->button_pic = gtk_image_new_from_resource(self->resource_path); // Create a new image from the resource
gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size);
gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget
gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box
}
}
void koto_button_set_text(
KotoButton * self,
gchar * text
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (!koto_utils_string_is_valid(text)) { // Invalid text
return;
}
if (koto_utils_string_is_valid(self->text)) { // Text defined
g_free(self->text); // Free existing text
}
self->text = g_strdup(text);
if (GTK_IS_LABEL(self->button_label)) { // If we have a button label
if (koto_utils_string_is_valid(self->text)) { // Have text set
gtk_label_set_text(GTK_LABEL(self->button_label), self->text);
gtk_widget_show(self->button_label); // Show the label
} else { // Have a label but no longer text
gtk_box_remove(GTK_BOX(self), self->button_label);
g_free(self->button_label);
}
} else { // If we do not have a button label
if (koto_utils_string_is_valid(self->text)) { // If we have text
self->button_label = gtk_label_new(self->text); // Create our label
gtk_widget_add_css_class(self->button_label, "button-label");
gtk_widget_set_hexpand(self->button_label, TRUE);
gtk_label_set_xalign(GTK_LABEL(self->button_label), 0);
if (GTK_IS_IMAGE(self->button_pic)) { // If we have an image
gtk_box_insert_child_after(GTK_BOX(self), self->button_label, self->button_pic);
} else {
gtk_box_prepend(GTK_BOX(self), GTK_WIDGET(self->button_label));
}
}
}
g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_TEXT]);
}
void koto_button_set_text_justify(
KotoButton * self,
GtkJustification j
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label
return;
}
if (j == GTK_JUSTIFY_CENTER) { // Center text
gtk_label_set_xalign(GTK_LABEL(self->button_label), 0.5);
} else if (j == GTK_JUSTIFY_RIGHT) { // Right align
gtk_label_set_xalign(GTK_LABEL(self->button_label), 1.0);
} else {
gtk_label_set_xalign(GTK_LABEL(self->button_label), 0);
}
gtk_label_set_justify(GTK_LABEL(self->button_label), j);
}
void koto_button_set_text_wrap(
KotoButton * self,
gboolean wrap
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label
return;
}
gtk_label_set_single_line_mode(GTK_LABEL(self->button_label), !wrap);
gtk_label_set_wrap(GTK_LABEL(self->button_label), wrap);
}
void koto_button_show_image(
KotoButton * self,
gboolean use_alt
) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (self->use_from_file) { // Use from a file instead of icon name
if (!koto_utils_string_is_valid(self->image_file_path)) { // Not set
return;
}
if (GTK_IS_IMAGE(self->button_pic)) { // Already have an icon
gtk_image_set_from_file(GTK_IMAGE(self->button_pic), self->image_file_path);
} else { // Don't have an image yet
self->button_pic = gtk_image_new_from_file(self->image_file_path); // Create a new image from the file
gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box
}
} else { // From icon name
if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set
return;
} else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set
return;
}
self->currently_showing_alt = use_alt;
gchar * name = use_alt ? self->alt_icon_name : self->icon_name;
if (GTK_IS_IMAGE(self->button_pic)) {
gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), name); // Just update the existing image
} else { // Not an image
self->button_pic = gtk_image_new_from_icon_name(name); // Get our new image
gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box
}
}
gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size);
gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget
gtk_widget_show(self->button_pic); // Ensure we actually are showing the image
}
void koto_button_unflatten(KotoButton * self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
gtk_widget_remove_css_class(GTK_WIDGET(self), "flat");
}
void koto_button_unset_pseudoactive_styling(KotoButton * self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if (!gtk_widget_has_css_class(GTK_WIDGET(self), "pseudoactive")) { // Don't have the CSS class
return;
}
gtk_widget_remove_css_class(GTK_WIDGET(self), "pseudoactive"); // Remove pseudoactive class
}
KotoButton * koto_button_new_plain(gchar * label) {
return g_object_new(
KOTO_TYPE_BUTTON,
"button-text",
label,
NULL
);
}
KotoButton * koto_button_new_with_icon(
gchar * label,
gchar * icon_name,
gchar * alt_icon_name,
KotoButtonPixbufSize size
) {
return g_object_new(
KOTO_TYPE_BUTTON,
"button-text",
label,
"icon-name",
icon_name,
"alt-icon-name",
alt_icon_name,
"pixbuf-size",
koto_get_pixbuf_size(size),
NULL
);
}
KotoButton * koto_button_new_with_file(
gchar * label,
gchar * file_path,
KotoButtonPixbufSize size
) {
return g_object_new(
KOTO_TYPE_BUTTON,
"button-text",
label,
"use-from-file",
TRUE,
"image-file-path",
file_path,
"pixbuf-size",
koto_get_pixbuf_size(size),
NULL
);
}
KotoButton * koto_button_new_with_resource (
gchar * resource_path,
KotoButtonPixbufSize size
) {
return g_object_new(
KOTO_TYPE_BUTTON,
"resource-path",
resource_path,
"pixbuf-size",
koto_get_pixbuf_size(size),
NULL
);
}

View file

@ -1,174 +0,0 @@
/* button.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h>
#include <gtk-4.0/gtk/gtk.h>
G_BEGIN_DECLS
typedef enum {
KOTO_BUTTON_CLICK_TYPE_PRIMARY = 1,
KOTO_BUTTON_CLICK_TYPE_SECONDARY = 3
} KotoButtonClickType;
typedef enum {
KOTO_BUTTON_PIXBUF_SIZE_INVALID,
KOTO_BUTTON_PIXBUF_SIZE_TINY,
KOTO_BUTTON_PIXBUF_SIZE_SMALL,
KOTO_BUTTON_PIXBUF_SIZE_NORMAL,
KOTO_BUTTON_PIXBUF_SIZE_LARGE,
KOTO_BUTTON_PIXBUF_SIZE_MASSIVE,
KOTO_BUTTON_PIXBUF_SIZE_GODLIKE
} KotoButtonPixbufSize;
typedef enum {
KOTO_BUTTON_IMAGE_POS_LEFT,
KOTO_BUTTON_IMAGE_POS_RIGHT
} KotoButtonImagePosition;
#define NUM_BUILTIN_SIZES 7
#define KOTO_TYPE_BUTTON (koto_button_get_type())
G_DECLARE_FINAL_TYPE(KotoButton, koto_button, KOTO, BUTTON, GtkBox)
#define KOTO_IS_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_BUTTON))
guint koto_get_pixbuf_size(KotoButtonPixbufSize size);
KotoButton * koto_button_new_plain(gchar * label);
KotoButton * koto_button_new_with_icon(
gchar * label,
gchar * icon_name,
gchar * alt_icon_name,
KotoButtonPixbufSize size
);
KotoButton * koto_button_new_with_file(
gchar * label,
gchar * file_path,
KotoButtonPixbufSize size
);
KotoButton * koto_button_new_with_resource(
gchar * resource_path,
KotoButtonPixbufSize size
);
void koto_button_add_click_handler(
KotoButton * self,
KotoButtonClickType button,
GCallback handler,
gpointer user_data
);
void koto_button_flip(KotoButton * self);
gpointer koto_button_get_data(KotoButton * self);
void koto_button_global_page_nav_callback(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_button_handle_mouse_enter(
GtkEventControllerMotion * controller,
double x,
double y,
gpointer user_data
);
void koto_button_handle_mouse_leave(
GtkEventControllerMotion * controller,
gpointer user_data
);
void koto_button_hide_image(KotoButton * self);
void koto_button_set_data(
KotoButton * self,
gpointer data
);
void koto_button_set_pseudoactive_styling(KotoButton * self);
void koto_button_set_badge_text(
KotoButton * self,
gchar * text
);
void koto_button_set_file_path(
KotoButton * self,
gchar * file_path
);
void koto_button_set_icon_name(
KotoButton * self,
gchar * icon_name,
gboolean for_alt
);
void koto_button_set_image_position(
KotoButton * self,
KotoButtonImagePosition pos
);
void koto_button_set_resource_path(
KotoButton * self,
gchar * resource_path
);
void koto_button_set_pixbuf(
KotoButton * self,
GdkPixbuf * pix
);
void koto_button_set_pixbuf_size(
KotoButton * self,
guint size
);
void koto_button_set_text(
KotoButton * self,
gchar * text
);
void koto_button_set_text_justify(
KotoButton * self,
GtkJustification j
);
void koto_button_set_text_wrap(
KotoButton * self,
gboolean wrap
);
void koto_button_show_image(
KotoButton * self,
gboolean use_alt
);
void koto_button_unflatten(KotoButton * self);
void koto_button_unset_pseudoactive_styling(KotoButton * self);
G_END_DECLS

View file

@ -1,269 +0,0 @@
/* cover-art-button.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../koto-utils.h"
#include "button.h"
#include "cover-art-button.h"
struct _KotoCoverArtButton {
GObject parent_instance;
GtkWidget * art;
GtkWidget * main;
GtkWidget * revealer;
KotoButton * play_button;
gchar * art_path;
guint height;
guint width;
};
G_DEFINE_TYPE(KotoCoverArtButton, koto_cover_art_button, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_DESIRED_HEIGHT,
PROP_DESIRED_WIDTH,
PROP_ART_PATH,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
static void koto_cover_art_button_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_cover_art_button_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_cover_art_button_class_init(KotoCoverArtButtonClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->get_property = koto_cover_art_button_get_property;
gobject_class->set_property = koto_cover_art_button_set_property;
props[PROP_DESIRED_HEIGHT] = g_param_spec_uint(
"desired-height",
"Desired height",
"Desired height",
0,
G_MAXUINT,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_DESIRED_WIDTH] = g_param_spec_uint(
"desired-width",
"Desired width",
"Desired width",
0,
G_MAXUINT,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_ART_PATH] = g_param_spec_string(
"art-path",
"Path to art",
"Path to art",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_cover_art_button_init(KotoCoverArtButton * self) {
self->main = gtk_overlay_new(); // Create our overlay container
gtk_widget_add_css_class(self->main, "cover-art-button");
self->revealer = gtk_revealer_new(); // Create a new revealer
gtk_revealer_set_transition_type(GTK_REVEALER(self->revealer), GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
gtk_revealer_set_transition_duration(GTK_REVEALER(self->revealer), 400);
GtkWidget * controls = gtk_center_box_new(); // Create a center box for the controls
self->play_button = koto_button_new_with_icon("", "media-playback-start-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_button));
gtk_revealer_set_child(GTK_REVEALER(self->revealer), controls);
koto_cover_art_button_hide_overlay_controls(NULL, self); // Hide by default
gtk_overlay_add_overlay(GTK_OVERLAY(self->main), self->revealer); // Add our revealer as the overlay
GtkEventController * motion_controller = gtk_event_controller_motion_new(); // Create our new motion event controller to track mouse leave and enter
g_signal_connect(motion_controller, "enter", G_CALLBACK(koto_cover_art_button_show_overlay_controls), self);
g_signal_connect(motion_controller, "leave", G_CALLBACK(koto_cover_art_button_hide_overlay_controls), self);
gtk_widget_add_controller(self->main, motion_controller);
self->art_path = NULL;
}
static void koto_cover_art_button_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
(void) val;
switch (prop_id) {
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_cover_art_button_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoCoverArtButton * self = KOTO_COVER_ART_BUTTON(obj);
switch (prop_id) {
case PROP_ART_PATH:
koto_cover_art_button_set_art_path(self, (gchar*) g_value_get_string(val)); // Get the value and call our set_art_path with it
break;
case PROP_DESIRED_HEIGHT:
koto_cover_art_button_set_dimensions(self, g_value_get_uint(val), 0);
break;
case PROP_DESIRED_WIDTH:
koto_cover_art_button_set_dimensions(self, 0, g_value_get_uint(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_cover_art_button_hide_overlay_controls(
GtkEventControllerFocus * controller,
gpointer data
) {
(void) controller;
KotoCoverArtButton * self = data;
gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), FALSE);
}
KotoButton * koto_cover_art_button_get_button(KotoCoverArtButton * self) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return NULL;
}
return self->play_button;
}
GtkWidget * koto_cover_art_button_get_main(KotoCoverArtButton * self) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return NULL;
}
return self->main;
}
void koto_cover_art_button_set_art_path(
KotoCoverArtButton * self,
gchar * art_path
) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return;
}
gboolean defined_artwork = koto_utils_string_is_valid(art_path);
if (GTK_IS_IMAGE(self->art)) { // Already have an image
if (!defined_artwork) { // No art path or empty string
gtk_image_set_from_icon_name(GTK_IMAGE(self->art), "audio-x-generic-symbolic");
} else { // Have an art path
if (g_strcmp0(self->art_path, art_path) != 0) {
self->art_path = g_strdup(art_path); // Set our art path
gtk_image_set_from_file(GTK_IMAGE(self->art), self->art_path); // Set from the file
}
}
} else { // If we don't have an image
self->art = koto_utils_create_image_from_filepath(defined_artwork ? g_strdup(art_path) : NULL, "audio-x-generic-symbolic", self->width, self->height);
gtk_overlay_set_child(GTK_OVERLAY(self->main), self->art); // Set the child
}
}
void koto_cover_art_button_set_dimensions(
KotoCoverArtButton * self,
guint height,
guint width
) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return;
}
if (height != 0) {
self->height = height;
}
if (width != 0) {
self->width = width;
}
if ((self->height != 0) && (self->width != 0)) { // Both height and width set
gtk_widget_set_size_request(self->main, self->width, self->height); // Update our widget
if (GTK_IS_IMAGE(self->art)) { // Art is defined
gtk_widget_set_size_request(self->art, self->width, self->height); // Update our image as well
}
}
}
void koto_cover_art_button_show_overlay_controls(
GtkEventControllerFocus * controller,
gpointer data
) {
(void) controller;
KotoCoverArtButton * self = data;
gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), TRUE);
}
KotoCoverArtButton * koto_cover_art_button_new(
guint height,
guint width,
gchar * art_path
) {
return g_object_new(
KOTO_TYPE_COVER_ART_BUTTON,
"desired-height",
height,
"desired-width",
width,
"art-path",
art_path,
NULL
);
}

View file

@ -1,63 +0,0 @@
/* cover-art-button.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "button.h"
G_BEGIN_DECLS
#define KOTO_TYPE_COVER_ART_BUTTON (koto_cover_art_button_get_type())
G_DECLARE_FINAL_TYPE(KotoCoverArtButton, koto_cover_art_button, KOTO, COVER_ART_BUTTON, GObject);
#define KOTO_IS_COVER_ART_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_COVER_ART_BUTTON))
/**
* Cover Art Functions
**/
KotoCoverArtButton * koto_cover_art_button_new(
guint height,
guint width,
gchar * art_path
);
KotoButton * koto_cover_art_button_get_button(KotoCoverArtButton * self);
GtkWidget * koto_cover_art_button_get_main(KotoCoverArtButton * self);
void koto_cover_art_button_hide_overlay_controls(
GtkEventControllerFocus * controller,
gpointer data
);
void koto_cover_art_button_set_art_path(
KotoCoverArtButton * self,
gchar * art_path
);
void koto_cover_art_button_set_dimensions(
KotoCoverArtButton * self,
guint height,
guint width
);
void koto_cover_art_button_show_overlay_controls(
GtkEventControllerFocus * controller,
gpointer data
);
G_END_DECLS

View file

@ -1,158 +0,0 @@
/* track-item.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtk-4.0/gtk/gtk.h>
#include "indexer/structs.h"
#include "playlist/add-remove-track-popover.h"
#include "button.h"
#include "track-item.h"
extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup;
struct _KotoTrackItem {
GtkBox parent_instance;
KotoTrack * track;
GtkWidget * track_label;
};
struct _KotoTrackItemClass {
GtkBoxClass parent_class;
};
enum {
PROP_0,
PROP_TRACK,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
G_DEFINE_TYPE(KotoTrackItem, koto_track_item, GTK_TYPE_BOX);
static void koto_track_item_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_track_item_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_track_item_class_init(KotoTrackItemClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_track_item_set_property;
gobject_class->get_property = koto_track_item_get_property;
props[PROP_TRACK] = g_param_spec_object(
"track",
"Track",
"Track",
KOTO_TYPE_TRACK,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_track_item_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoTrackItem * self = KOTO_TRACK_ITEM(obj);
switch (prop_id) {
case PROP_TRACK:
g_value_set_object(val, self->track);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_track_item_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoTrackItem * self = KOTO_TRACK_ITEM(obj);
switch (prop_id) {
case PROP_TRACK:
koto_track_item_set_track(self, (KotoTrack*) g_value_get_object(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_track_item_init(KotoTrackItem * self) {
self->track_label = gtk_label_new(NULL); // Create with no track name
gtk_label_set_xalign(GTK_LABEL(self->track_label), 0.0);
gtk_widget_add_css_class(GTK_WIDGET(self), "track-item");
gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(self->track_label), TRUE);
gtk_box_prepend(GTK_BOX(self), self->track_label);
}
KotoTrack * koto_track_item_get_track(KotoTrackItem * self) {
if (!KOTO_IS_TRACK_ITEM(self)) {
return NULL;
}
return self->track;
}
void koto_track_item_set_track(
KotoTrackItem * self,
KotoTrack * track
) {
if (track == NULL) { // Not a track
return;
}
self->track = track;
gchar * track_name;
g_object_get(self->track, "parsed-name", &track_name, NULL);
gtk_label_set_text(GTK_LABEL(self->track_label), track_name); // Update the text
}
KotoTrackItem * koto_track_item_new(KotoTrack * track) {
return g_object_new(
KOTO_TYPE_TRACK_ITEM,
"track",
track,
NULL
);
}

View file

@ -1,46 +0,0 @@
/* track-item.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "indexer/structs.h"
G_BEGIN_DECLS
#define KOTO_TYPE_TRACK_ITEM (koto_track_item_get_type())
G_DECLARE_FINAL_TYPE(KotoTrackItem, koto_track_item, KOTO, TRACK_ITEM, GtkBox)
KotoTrackItem* koto_track_item_new(KotoTrack * track);
void koto_track_item_handle_add_to_playlist_button_click(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
KotoTrack * koto_track_item_get_track(KotoTrackItem * self);
void koto_track_item_set_track(
KotoTrackItem * self,
KotoTrack * track
);
G_END_DECLS

View file

@ -1,540 +0,0 @@
/* track-table.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../db/cartographer.h"
#include "../playback/engine.h"
#include "../playlist/current.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "../koto-window.h"
#include "action-bar.h"
#include "button.h"
#include "track-table.h"
extern KotoActionBar * action_bar;
extern KotoCartographer * koto_maps;
extern KotoCurrentPlaylist * current_playlist;
extern KotoPlaybackEngine * playback_engine;
extern KotoWindow * main_window;
struct _KotoTrackTable {
GObject parent_instance;
gchar * uuid;
KotoPlaylist * playlist;
GtkWidget * main;
GtkListItemFactory * item_factory;
GListModel * model;
GtkSelectionModel * selection_model;
GtkWidget * track_list_content;
GtkWidget * track_list_header;
GtkWidget * track_list_view;
KotoButton * track_album_button;
KotoButton * track_artist_button;
KotoButton * track_num_button;
KotoButton * track_title_button;
GtkSizeGroup * track_pos_size_group;
GtkSizeGroup * track_name_size_group;
GtkSizeGroup * track_album_size_group;
GtkSizeGroup * track_artist_size_group;
};
struct _KotoTrackTableClass {
GObjectClass parent_class;
};
G_DEFINE_TYPE(KotoTrackTable, koto_track_table, G_TYPE_OBJECT);
static void koto_track_table_class_init(KotoTrackTableClass * c) {
(void) c;
}
static void koto_track_table_init(KotoTrackTable * self) {
self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
self->track_name_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_pos_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_album_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_artist_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->item_factory = gtk_signal_list_item_factory_new(); // Create a new signal list item factory
g_signal_connect(self->item_factory, "setup", G_CALLBACK(koto_track_table_setup_track_item), self);
g_signal_connect(self->item_factory, "bind", G_CALLBACK(koto_track_table_bind_track_item), self);
self->track_list_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class((self->track_list_content), "track-list-content");
gtk_widget_set_hexpand(self->track_list_content, TRUE); // Expand horizontally
gtk_widget_set_vexpand(self->track_list_content, TRUE); // Expand vertically
self->track_list_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->track_list_header, "track-list-header");
koto_track_table_create_tracks_header(self); // Create our tracks header content
self->track_list_view = gtk_list_view_new(NULL, self->item_factory); // Create our list view with no model yet
gtk_widget_add_css_class(self->track_list_view, "track-list-columned");
gtk_widget_set_hexpand(self->track_list_view, TRUE); // Expand horizontally
gtk_widget_set_vexpand(self->track_list_view, TRUE); // Expand vertically
gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_header);
gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_view);
gtk_box_append(GTK_BOX(self->main), self->track_list_content);
g_signal_connect(action_bar, "closed", G_CALLBACK(koto_track_table_handle_action_bar_closed), self); // Handle closed action bar
}
void koto_track_table_bind_track_item(
GtkListItemFactory * factory,
GtkListItem * item,
KotoTrackTable * self
) {
(void) factory;
GtkWidget * box = gtk_list_item_get_child(item);
GtkWidget * track_position_label = gtk_widget_get_first_child(box);
GtkWidget * track_name_label = gtk_widget_get_next_sibling(track_position_label);
GtkWidget * track_album_label = gtk_widget_get_next_sibling(track_name_label);
GtkWidget * track_artist_label = gtk_widget_get_next_sibling(track_album_label);
KotoTrack * track = gtk_list_item_get_item(item); // Get the track UUID from our model
if (!KOTO_IS_TRACK(track)) {
return;
}
gchar * track_name = koto_track_get_name(track);
gchar * album_uuid = NULL;
gchar * artist_uuid = NULL;
g_object_get(
track,
"album-uuid",
&album_uuid,
"artist-uuid",
&artist_uuid,
NULL
);
guint track_position = koto_playlist_get_position_of_track(self->playlist, track) + 1;
gtk_label_set_label(GTK_LABEL(track_position_label), g_strdup_printf("%u", track_position)); // Set the track position
gtk_label_set_label(GTK_LABEL(track_name_label), track_name); // Set our track name
if (koto_utils_string_is_valid(album_uuid)) { // Is associated with an album
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid);
if (KOTO_IS_ALBUM(album)) {
gtk_label_set_label(GTK_LABEL(track_album_label), koto_album_get_name(album)); // Get the name of the album and set it to the label
}
}
if (koto_utils_string_is_valid(artist_uuid)) { // Is associated with an artist
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid);
if (KOTO_IS_ARTIST(artist)) {
gtk_label_set_label(GTK_LABEL(track_artist_label), koto_artist_get_name(artist)); // Get the name of the artist and set it to the label
}
}
GList * data = NULL;
data = g_list_append(data, self); // Reference self first
data = g_list_append(data, koto_track_get_uuid(track)); // Next reference the track UUID string
g_object_set_data(G_OBJECT(box), "data", data); // Bind our list data
}
void koto_track_table_create_tracks_header(KotoTrackTable * self) {
self->track_num_button = koto_button_new_with_icon("#", "pan-down-symbolic", "pan-up-symbolic", KOTO_BUTTON_PIXBUF_SIZE_SMALL);
koto_button_add_click_handler(self->track_num_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_track_table_handle_track_num_clicked), self);
koto_button_set_image_position(self->track_num_button, KOTO_BUTTON_IMAGE_POS_RIGHT); // Move the image to the right
gtk_size_group_add_widget(self->track_pos_size_group, GTK_WIDGET(self->track_num_button));
self->track_title_button = koto_button_new_plain("Title");
koto_button_add_click_handler(self->track_title_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_track_table_handle_track_name_clicked), self);
gtk_size_group_add_widget(self->track_name_size_group, GTK_WIDGET(self->track_title_button));
self->track_album_button = koto_button_new_plain("Album");
gtk_widget_set_margin_start(GTK_WIDGET(self->track_album_button), 50);
koto_button_add_click_handler(self->track_album_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_track_table_handle_track_album_clicked), self);
gtk_size_group_add_widget(self->track_album_size_group, GTK_WIDGET(self->track_album_button));
self->track_artist_button = koto_button_new_plain("Artist");
gtk_widget_set_margin_start(GTK_WIDGET(self->track_artist_button), 50);
koto_button_add_click_handler(self->track_artist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_track_table_handle_track_artist_clicked), self);
gtk_size_group_add_widget(self->track_artist_size_group, GTK_WIDGET(self->track_artist_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_num_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_title_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_album_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_artist_button));
}
GtkWidget * koto_track_table_get_main(KotoTrackTable * self) {
return self->main;
}
void koto_track_table_handle_action_bar_closed (
KotoActionBar * bar,
gpointer data
) {
(void) bar;
KotoTrackTable * self = data;
if (!KOTO_IS_TRACK_TABLE(self)) { // Self is not a track table
return;
}
if (!KOTO_IS_PLAYLIST(self->playlist)) { // No playlist set
return;
}
gtk_selection_model_unselect_all(self->selection_model);
gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window
}
void koto_track_table_handle_track_album_clicked (
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoTrackTable * self = user_data;
gtk_widget_add_css_class(GTK_WIDGET(self->track_album_button), "active");
koto_button_hide_image(self->track_num_button); // Go back to hiding the image
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM);
}
void koto_track_table_handle_track_artist_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoTrackTable * self = user_data;
gtk_widget_add_css_class(GTK_WIDGET(self->track_artist_button), "active");
koto_button_hide_image(self->track_num_button); // Go back to hiding the image
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST);
}
void koto_track_table_item_handle_clicked(
GtkGesture * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) x;
(void) y;
if (n_press != 2) { // Not a double click or tap
return;
}
GObject * track_item_as_object = G_OBJECT(user_data);
if (!G_IS_OBJECT(track_item_as_object)) { // Not a GObject
return;
}
GList * data = g_object_get_data(track_item_as_object, "data");
KotoTrackTable * self = g_list_nth_data(data, 0);
gchar * track_uuid = g_list_nth_data(data, 1);
if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid string
return;
}
gtk_selection_model_unselect_all(self->selection_model);
gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window
koto_action_bar_toggle_reveal(action_bar, FALSE);
koto_action_bar_close(action_bar); // Close the action bar
koto_current_playlist_set_playlist(current_playlist, self->playlist, FALSE, FALSE); // Set the current playlist to the artist's playlist but do not play immediately
koto_playlist_set_track_as_current(self->playlist, track_uuid); // Set this track as the current one for the playlist
koto_playback_engine_set_track_by_uuid(playback_engine, track_uuid, FALSE); // Tell our playback engine to start playing at this track
}
void koto_track_table_handle_track_name_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoTrackTable * self = user_data;
gtk_widget_add_css_class(GTK_WIDGET(self->track_title_button), "active");
koto_button_hide_image(self->track_num_button); // Go back to hiding the image
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME);
}
void koto_track_table_handle_track_num_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoTrackTable * self = user_data;
KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist);
if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) { // Set to newest currently
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST); // Sort reversed (oldest)
koto_button_show_image(self->track_num_button, TRUE); // Use inverted value (pan-up-symbolic)
} else {
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // Sort newest
koto_button_show_image(self->track_num_button, FALSE); // Use pan down default
}
}
void koto_track_table_handle_tracks_selected(
GtkSelectionModel * model,
guint position,
guint n_items,
gpointer user_data
) {
(void) position;
KotoTrackTable * self = user_data;
if (!KOTO_IS_TRACK_TABLE(self)) { // Not a track table
return;
}
if (n_items == 0) { // No items selected
koto_action_bar_toggle_reveal(action_bar, FALSE); // Hide the action bar
return;
}
GtkBitset * selected_items_bitset = gtk_selection_model_get_selection(model); // Get the selected items as a GtkBitSet
GtkBitsetIter iter;
GList * selected_tracks = NULL;
GList * selected_tracks_pos = NULL;
guint first_track_pos;
if (!gtk_bitset_iter_init_first(&iter, selected_items_bitset, &first_track_pos)) { // Failed to get the first item
return;
}
selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(first_track_pos));
gboolean have_more_items = TRUE;
while (have_more_items) { // While we are able to get selected items
guint track_pos;
have_more_items = gtk_bitset_iter_next(&iter, &track_pos);
if (have_more_items) { // Got the next track
selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(track_pos));
}
}
GList * cur_pos_list;
for (cur_pos_list = selected_tracks_pos; cur_pos_list != NULL; cur_pos_list = cur_pos_list->next) { // Iterate over every position that we accumulated
KotoTrack * selected_track = g_list_model_get_item(self->model, GPOINTER_TO_UINT(cur_pos_list->data)); // Get the KotoTrack in the GListModel for this current position
selected_tracks = g_list_append(selected_tracks, selected_track); // Add to selected tracks
}
koto_action_bar_set_tracks_in_playlist_selection(action_bar, koto_playlist_get_uuid(self->playlist), selected_tracks); // Set the tracks for the playlist selection
koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the items
}
void koto_track_table_set_model(
KotoTrackTable * self,
KotoPreferredPlaylistSortType model
) {
if (!KOTO_IS_TRACK_TABLE(self)) {
return;
}
koto_playlist_apply_model(self->playlist, model); // Apply our new model
self->model = G_LIST_MODEL(koto_playlist_get_store(self->playlist)); // Get the latest generated model / store and cast it as a GListModel
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently
gtk_widget_remove_css_class(GTK_WIDGET(self->track_album_button), "active");
}
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently
gtk_widget_remove_css_class(GTK_WIDGET(self->track_artist_button), "active");
}
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name
gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active");
}
self->selection_model = GTK_SELECTION_MODEL(gtk_multi_selection_new(self->model));
g_signal_connect(self->selection_model, "selection-changed", G_CALLBACK(koto_track_table_handle_tracks_selected), self); // Bind to our selection changed
gtk_list_view_set_model(GTK_LIST_VIEW(self->track_list_view), self->selection_model); // Set our multi selection model to our provided model
}
void koto_track_table_set_playlist_model (
KotoTrackTable * self,
KotoPreferredPlaylistSortType model
) {
if (!KOTO_IS_TRACK_TABLE(self)) { // Not a track table
return;
}
if (!KOTO_IS_PLAYLIST(self->playlist)) { // Don't have a playlist yet
return;
}
koto_playlist_apply_model(self->playlist, model); // Apply our new model
self->model = G_LIST_MODEL(koto_playlist_get_store(self->playlist)); // Get the latest generated model / store and cast it as a GListModel
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently
gtk_widget_remove_css_class(GTK_WIDGET(self->track_album_button), "active");
}
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently
gtk_widget_remove_css_class(GTK_WIDGET(self->track_artist_button), "active");
}
if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name
gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active");
}
self->selection_model = GTK_SELECTION_MODEL(gtk_multi_selection_new(self->model));
g_signal_connect(self->selection_model, "selection-changed", G_CALLBACK(koto_track_table_handle_tracks_selected), self); // Bind to our selection changed
gtk_list_view_set_model(GTK_LIST_VIEW(self->track_list_view), self->selection_model); // Set our multi selection model to our provided model
KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model
if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST) {
koto_button_show_image(self->track_num_button, TRUE); // Immediately use pan-up-symbolic
}
}
void koto_track_table_set_playlist(
KotoTrackTable * self,
KotoPlaylist * playlist
) {
if (!KOTO_IS_TRACK_TABLE(self)) {
return;
}
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
return;
}
self->playlist = playlist;
koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // TODO: Enable this to be changed
}
void koto_track_table_setup_track_item(
GtkListItemFactory * factory,
GtkListItem * item,
gpointer user_data
) {
(void) factory;
KotoTrackTable * self = user_data;
if (!KOTO_IS_TRACK_TABLE(self)) {
return;
}
GtkWidget * item_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Have a horizontal box for our content
gtk_widget_add_css_class(item_content, "track-list-columned-item");
GtkWidget * track_number = gtk_label_new(NULL); // Our track number
gtk_label_set_xalign(GTK_LABEL(track_number), 0);
gtk_widget_add_css_class(track_number, "track-column-number");
gtk_widget_set_halign(track_number, GTK_ALIGN_END);
gtk_widget_set_hexpand(track_number, FALSE);
gtk_widget_set_size_request(track_number, 150, -1);
gtk_box_append(GTK_BOX(item_content), track_number);
gtk_size_group_add_widget(self->track_pos_size_group, track_number);
GtkWidget * track_name = gtk_label_new(NULL); // Our track name
gtk_label_set_xalign(GTK_LABEL(track_name), 0);
gtk_widget_add_css_class(track_name, "track-column-name");
gtk_widget_set_halign(track_name, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_name, FALSE);
gtk_widget_set_size_request(track_name, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_name);
gtk_size_group_add_widget(self->track_name_size_group, track_name);
GtkWidget * track_album = gtk_label_new(NULL); // Our track album
gtk_label_set_xalign(GTK_LABEL(track_album), 0);
gtk_widget_add_css_class(track_album, "track-column-album");
gtk_widget_set_halign(track_album, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_album, FALSE);
gtk_widget_set_margin_start(track_album, 50);
gtk_widget_set_size_request(track_album, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_album);
gtk_size_group_add_widget(self->track_album_size_group, track_album);
GtkWidget * track_artist = gtk_label_new(NULL); // Our track artist
gtk_label_set_xalign(GTK_LABEL(track_artist), 0);
gtk_widget_add_css_class(track_artist, "track-column-artist");
gtk_widget_set_halign(track_artist, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_artist, TRUE);
gtk_widget_set_margin_start(track_artist, 50);
gtk_widget_set_size_request(track_artist, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_artist);
gtk_size_group_add_widget(self->track_artist_size_group, track_artist);
gtk_list_item_set_child(item, item_content);
GtkGesture * double_click_gesture = gtk_gesture_click_new(); // Create our new GtkGesture for double-click handling
gtk_widget_add_controller(item_content, GTK_EVENT_CONTROLLER(double_click_gesture)); // Have our item handle double clicking
g_signal_connect(double_click_gesture, "released", G_CALLBACK(koto_track_table_item_handle_clicked), item_content);
}
KotoTrackTable * koto_track_table_new() {
return g_object_new(
KOTO_TYPE_TRACK_TABLE,
NULL
);
}

View file

@ -1,122 +0,0 @@
/* track-table.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../playlist/playlist.h"
#include "action-bar.h"
G_BEGIN_DECLS
#define KOTO_TYPE_TRACK_TABLE koto_track_table_get_type()
#define KOTO_TRACK_TABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_TRACK_TABLE, KotoTrackTable))
typedef struct _KotoTrackTable KotoTrackTable;
typedef struct _KotoTrackTableClass KotoTrackTableClass;
GLIB_AVAILABLE_IN_ALL
GType koto_track_type_get_type(void) G_GNUC_CONST;
#define KOTO_IS_TRACK_TABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_TRACK_TABLE))
void koto_track_table_bind_track_item(
GtkListItemFactory * factory,
GtkListItem * item,
KotoTrackTable * self
);
void koto_track_table_create_tracks_header(KotoTrackTable * self);
GtkWidget * koto_track_table_get_main(KotoTrackTable * self);
void koto_track_table_handle_action_bar_closed(
KotoActionBar * bar,
gpointer data
);
void koto_track_table_handle_track_album_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_track_table_handle_track_artist_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_track_table_handle_track_name_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_track_table_handle_track_name_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_track_table_handle_track_num_clicked(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_track_table_handle_tracks_selected(
GtkSelectionModel * model,
guint position,
guint n_items,
gpointer user_data
);
void koto_track_table_set_model(
KotoTrackTable * self,
KotoPreferredPlaylistSortType model
);
void koto_track_table_set_playlist_model(
KotoTrackTable * self,
KotoPreferredPlaylistSortType model
);
void koto_track_table_set_playlist(
KotoTrackTable * self,
KotoPlaylist * playlist
);
void koto_track_table_setup_track_item(
GtkListItemFactory * factory,
GtkListItem * item,
gpointer user_data
);
KotoTrackTable * koto_track_table_new();
G_END_DECLS

View file

@ -1,768 +0,0 @@
/* config.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <glib-2.0/gio/gio.h>
#include <errno.h>
#include <toml.h>
#include "../db/cartographer.h"
#include "../playback/engine.h"
#include "../koto-paths.h"
#include "../koto-utils.h"
#include "config.h"
extern int errno;
extern const gchar * koto_config_template;
extern KotoCartographer * koto_maps;
extern KotoPlaybackEngine * playback_engine;
enum {
PROP_0,
PROP_PLAYBACK_CONTINUE_ON_PLAYLIST,
PROP_PLAYBACK_LAST_USED_VOLUME,
PROP_PLAYBACK_MAINTAIN_SHUFFLE,
PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT,
PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT,
PROP_PREFERRED_ALBUM_SORT_TYPE,
PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION,
PROP_UI_ALBUM_INFO_SHOW_GENRES,
PROP_UI_ALBUM_INFO_SHOW_NARRATOR,
PROP_UI_ALBUM_INFO_SHOW_YEAR,
PROP_UI_THEME_DESIRED,
PROP_UI_THEME_OVERRIDE,
N_PROPS,
};
static GParamSpec * config_props[N_PROPS] = {
0
};
struct _KotoConfig {
GObject parent_instance;
toml_table_t * toml_ref;
GFile * config_file;
GFileMonitor * config_file_monitor;
gchar * path;
gboolean finalized;
/* Library Attributes */
// These are useful for when we need to determine separately if we need to index initial builtin folders that did not exist previously (during load)
gboolean has_type_audiobook;
gboolean has_type_music;
gboolean has_type_podcast;
/* Playback Settings */
gboolean playback_continue_on_playlist;
gdouble playback_last_used_volume;
gboolean playback_maintain_shuffle;
guint playback_jump_backwards_increment;
guint playback_jump_forwards_increment;
/* Misc Prefs */
KotoPreferredAlbumSortType preferred_album_sort_type;
/* UI Settings */
gboolean ui_album_info_show_description;
gboolean ui_album_info_show_genres;
gboolean ui_album_info_show_narrator;
gboolean ui_album_info_show_year;
gchar * ui_theme_desired;
gboolean ui_theme_override;
};
struct _KotoConfigClass {
GObjectClass parent_class;
};
G_DEFINE_TYPE(KotoConfig, koto_config, G_TYPE_OBJECT);
KotoConfig * config;
static void koto_config_constructed(GObject * obj);
static void koto_config_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_config_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_config_class_init(KotoConfigClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->constructed = koto_config_constructed;
gobject_class->get_property = koto_config_get_property;
gobject_class->set_property = koto_config_set_property;
config_props[PROP_PLAYBACK_CONTINUE_ON_PLAYLIST] = g_param_spec_boolean(
"playback-continue-on-playlist",
"Continue Playback of Playlist",
"Continue playback of a Playlist after playing a specific track in the playlist",
FALSE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_LAST_USED_VOLUME] = g_param_spec_double(
"playback-last-used-volume",
"Last Used Volume",
"Last Used Volume",
0,
1,
0.5, // 50%
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_MAINTAIN_SHUFFLE] = g_param_spec_boolean(
"playback-maintain-shuffle",
"Maintain Shuffle on Playlist Change",
"Maintain shuffle setting when changing playlists",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT] = g_param_spec_uint(
"playback-jump-backwards-increment",
"Jump Backwards Increment",
"Jump Backwards Increment",
5, // 5s
90, // 1min30s
10, // 10s
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT] = g_param_spec_uint(
"playback-jump-forwards-increment",
"Jump Forwards Increment",
"Jump Forwards Increment",
5, // 5s
90, // 1min30s
30, // 30s
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PREFERRED_ALBUM_SORT_TYPE] = g_param_spec_string(
"artist-preferred-album-sort-type",
"Preferred album sort type (chronological or alphabetical-only)",
"Preferred album sort type (chronological or alphabetical-only)",
"default",
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION] = g_param_spec_boolean(
"ui-album-info-show-description",
"Show Description in Album Info",
"Show Description in Album Info",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_ALBUM_INFO_SHOW_GENRES] = g_param_spec_boolean(
"ui-album-info-show-genres",
"Show Genres in Album Info",
"Show Genres in Album Info",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_ALBUM_INFO_SHOW_NARRATOR] = g_param_spec_boolean(
"ui-album-info-show-narrator",
"Show Narrator in Album Info",
"Show Narrator in Album Info",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_ALBUM_INFO_SHOW_YEAR] = g_param_spec_boolean(
"ui-album-info-show-year",
"Show Year in Album Info",
"Show Year in Album Info",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_THEME_DESIRED] = g_param_spec_string(
"ui-theme-desired",
"Desired Theme",
"Desired Theme",
"dark", // Like my soul
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_THEME_OVERRIDE] = g_param_spec_boolean(
"ui-theme-override",
"Override built-in theming",
"Override built-in theming",
FALSE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPS, config_props);
}
static void koto_config_init(KotoConfig * self) {
self->finalized = FALSE;
self->has_type_audiobook = FALSE;
self->has_type_music = FALSE;
self->has_type_podcast = FALSE;
}
static void koto_config_constructed(GObject * obj) {
KotoConfig * self = KOTO_CONFIG(obj);
self->finalized = TRUE;
}
static void koto_config_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoConfig * self = KOTO_CONFIG(obj);
switch (prop_id) {
case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST:
g_value_set_boolean(val, self->playback_continue_on_playlist);
break;
case PROP_PLAYBACK_LAST_USED_VOLUME:
g_value_set_double(val, self->playback_last_used_volume);
break;
case PROP_PLAYBACK_MAINTAIN_SHUFFLE:
g_value_set_boolean(val, self->playback_maintain_shuffle);
break;
case PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT:
g_value_set_uint(val, self->playback_jump_backwards_increment);
break;
case PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT:
g_value_set_uint(val, self->playback_jump_forwards_increment);
break;
case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION:
g_value_set_boolean(val, self->ui_album_info_show_description);
break;
case PROP_UI_ALBUM_INFO_SHOW_GENRES:
g_value_set_boolean(val, self->ui_album_info_show_genres);
break;
case PROP_UI_ALBUM_INFO_SHOW_NARRATOR:
g_value_set_boolean(val, self->ui_album_info_show_narrator);
break;
case PROP_UI_ALBUM_INFO_SHOW_YEAR:
g_value_set_boolean(val, self->ui_album_info_show_year);
break;
case PROP_UI_THEME_DESIRED:
g_value_set_string(val, g_strdup(self->ui_theme_desired));
break;
case PROP_UI_THEME_OVERRIDE:
g_value_set_boolean(val, self->ui_theme_override);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_config_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoConfig * self = KOTO_CONFIG(obj);
switch (prop_id) {
case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST:
self->playback_continue_on_playlist = g_value_get_boolean(val);
break;
case PROP_PLAYBACK_LAST_USED_VOLUME:
self->playback_last_used_volume = g_value_get_double(val);
break;
case PROP_PLAYBACK_MAINTAIN_SHUFFLE:
self->playback_maintain_shuffle = g_value_get_boolean(val);
break;
case PROP_PLAYBACK_JUMP_BACKWARDS_INCREMENT:
self->playback_jump_backwards_increment = g_value_get_uint(val);
break;
case PROP_PLAYBACK_JUMP_FORWARDS_INCREMENT:
self->playback_jump_forwards_increment = g_value_get_uint(val);
break;
case PROP_PREFERRED_ALBUM_SORT_TYPE:
self->preferred_album_sort_type = g_strcmp0(g_value_get_string(val), "alphabetical") ? KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL : KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT;
break;
case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION:
self->ui_album_info_show_description = g_value_get_boolean(val);
break;
case PROP_UI_ALBUM_INFO_SHOW_GENRES:
self->ui_album_info_show_genres = g_value_get_boolean(val);
break;
case PROP_UI_ALBUM_INFO_SHOW_NARRATOR:
self->ui_album_info_show_narrator = g_value_get_boolean(val);
break;
case PROP_UI_ALBUM_INFO_SHOW_YEAR:
self->ui_album_info_show_year = g_value_get_boolean(val);
break;
case PROP_UI_THEME_DESIRED:
self->ui_theme_desired = g_strdup(g_value_get_string(val));
break;
case PROP_UI_THEME_OVERRIDE:
self->ui_theme_override = g_value_get_boolean(val);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
if (self->finalized) { // Loaded the config
g_object_notify_by_pspec(obj, config_props[prop_id]); // Notify that a change happened
}
}
toml_table_t * koto_config_get_library(
KotoConfig * self,
gchar * library_uuid
) {
toml_array_t * libraries = toml_array_in(self->toml_ref, "library"); // Get the array of tables
for (int i = 0; i < toml_array_nelem(libraries); i++) { // For each library
toml_table_t * library = toml_table_at(libraries, i); // Get this library
toml_datum_t uuid_datum = toml_string_in(library, "uuid"); // Get the datum for the uuid
gchar * lib_uuid = (uuid_datum.ok) ? (gchar*) uuid_datum.u.s : NULL;
if (koto_utils_string_is_valid(lib_uuid) && (g_strcmp0(library_uuid, lib_uuid) == 0)) { // Is a valid string and the libraries match
return library;
}
}
return NULL;
}
/**
* Load our TOML file from the specified path into our KotoConfig
**/
void koto_config_load(
KotoConfig * self,
gchar * path
) {
if (!koto_utils_string_is_valid(path)) { // Path is not valid
return;
}
self->path = g_strdup(path);
self->config_file = g_file_new_for_path(path);
gboolean config_file_exists = g_file_query_exists(self->config_file, NULL);
if (!config_file_exists) { // File does not exist
GError * create_err;
GFileOutputStream * stream = g_file_create(
self->config_file,
G_FILE_CREATE_PRIVATE,
NULL,
&create_err
);
if (create_err != NULL) {
if (create_err->code != G_IO_ERROR_EXISTS) { // Not an error indicating the file already exists
g_warning("Failed to create or open file: %s", create_err->message);
return;
}
}
g_object_unref(stream);
}
GError * file_info_query_err;
GFileInfo * file_info = g_file_query_info(
// Get the size of our TOML file
self->config_file,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
G_FILE_QUERY_INFO_NONE,
NULL,
&file_info_query_err
);
if (file_info != NULL) { // Got info
goffset size = g_file_info_get_size(file_info); // Get the size from info
g_object_unref(file_info); // Unref immediately
if (size == 0) { // If we don't have any file contents (new file), skip parsing
goto monitor;
}
} else { // Failed to get the info
g_warning("Failed to get size info of %s: %s", self->path, file_info_query_err->message);
}
FILE * file;
file = fopen(self->path, "r"); // Open the file as read only
if (file == NULL) { // Failed to get the file
/** Handle error checking here*/
return;
}
char errbuf[200];
toml_table_t * conf = toml_parse_file(file, errbuf, sizeof(errbuf));
fclose(file); // Close the file
if (!conf) {
g_error("Failed to read our config file. %s", errbuf);
return;
}
self->toml_ref = conf;
/** Supplemental Libraries (Excludes Built-in) */
toml_array_t * libraries = toml_array_in(conf, "library"); // Get all of our libraries
if (libraries) { // If we have libraries already
for (int i = 0; i < toml_array_nelem(libraries); i++) { // Iterate over each library
toml_table_t * lib = toml_table_at(libraries, i); // Get the library datum
KotoLibrary * koto_lib = koto_library_new_from_toml_table(lib); // Get the library based on the TOML data for this specific type
if (!KOTO_IS_LIBRARY(koto_lib)) { // Something wrong with it, not a library
continue;
}
koto_cartographer_add_library(koto_maps, koto_lib); // Add library to Cartographer
KotoLibraryType lib_type = koto_library_get_lib_type(koto_lib); // Get the type
if (lib_type == KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Is an audiobook lib
self->has_type_audiobook = TRUE;
} else if (lib_type == KOTO_LIBRARY_TYPE_MUSIC) { // Is a music lib
self->has_type_music = TRUE;
} else if (lib_type == KOTO_LIBRARY_TYPE_PODCAST) { // Is a podcast lib
self->has_type_podcast = TRUE;
}
}
}
/** Playback Section */
toml_table_t * playback_section = toml_table_in(conf, "playback");
if (playback_section) { // Have playback section
toml_datum_t continue_on_playlist = toml_bool_in(playback_section, "continue-on-playlist");
toml_datum_t jump_backwards_increment = toml_int_in(playback_section, "jump-backwards-increment");
toml_datum_t jump_forwards_increment = toml_int_in(playback_section, "jump-forwards-increment");
toml_datum_t last_used_volume = toml_double_in(playback_section, "last-used-volume");
toml_datum_t maintain_shuffle = toml_bool_in(playback_section, "maintain-shuffle");
if (continue_on_playlist.ok && (self->playback_continue_on_playlist != continue_on_playlist.u.b)) { // If we have a continue-on-playlist set and they are different
g_object_set(self, "playback-continue-on-playlist", continue_on_playlist.u.b, NULL);
}
if (jump_backwards_increment.ok && (self->playback_jump_backwards_increment != jump_backwards_increment.u.i)) { // If we have a jump-backwards-increment set and it is different
g_object_set(self, "playback-jump-backwards-increment", (guint) jump_backwards_increment.u.i, NULL);
}
if (jump_forwards_increment.ok && (self->playback_jump_forwards_increment != jump_forwards_increment.u.i)) { // If we have a jump-backwards-increment set and it is different
g_object_set(self, "playback-jump-forwards-increment", (guint) jump_forwards_increment.u.i, NULL);
}
if (last_used_volume.ok && (self->playback_last_used_volume != last_used_volume.u.d)) { // If we have last-used-volume set and they are different
g_object_set(self, "playback-last-used-volume", last_used_volume.u.d, NULL);
}
if (maintain_shuffle.ok && (self->playback_maintain_shuffle != maintain_shuffle.u.b)) { // If we have a "maintain shuffle set" and they are different
g_object_set(self, "playback-maintain-shuffle", maintain_shuffle.u.b, NULL);
}
}
/* UI Section */
toml_table_t * ui_section = toml_table_in(conf, "ui");
if (ui_section) { // Have UI section
toml_datum_t album_info_show_description = toml_bool_in(ui_section, "album-info-show-description");
if (album_info_show_description.ok && (album_info_show_description.u.b != self->ui_album_info_show_description)) { // Changed if we are disabling description
g_object_set(self, "ui-album-info-show-description", album_info_show_description.u.b, NULL);
}
toml_datum_t album_info_show_genres = toml_bool_in(ui_section, "album-info-show-genres");
if (album_info_show_genres.ok && (album_info_show_genres.u.b != self->ui_album_info_show_genres)) { // Changed if we are disabling description
g_object_set(self, "ui-album-info-show-genres", album_info_show_genres.u.b, NULL);
}
toml_datum_t album_info_show_narrator = toml_bool_in(ui_section, "album-info-show-narrator");
if (album_info_show_narrator.ok && (album_info_show_narrator.u.b != self->ui_album_info_show_narrator)) { // Changed if we are disabling description
g_object_set(self, "ui-album-info-show-narrator", album_info_show_description.u.b, NULL);
}
toml_datum_t album_info_show_year = toml_bool_in(ui_section, "album-info-show-year");
if (album_info_show_year.ok && (album_info_show_year.u.b != self->ui_album_info_show_year)) { // Changed if we are disabling description
g_object_set(self, "ui-album-info-show-year", album_info_show_year.u.b, NULL);
}
toml_datum_t name = toml_string_in(ui_section, "theme-desired");
if (name.ok && (g_strcmp0(name.u.s, self->ui_theme_desired) != 0)) { // Have a name specified and they are different
g_object_set(self, "ui-theme-desired", g_strdup(name.u.s), NULL);
free(name.u.s);
}
toml_datum_t override_app = toml_bool_in(ui_section, "theme-override");
if (override_app.ok && (override_app.u.b != self->ui_theme_override)) { // Changed if we are overriding theme
g_object_set(self, "ui-theme-override", override_app.u.b, NULL);
}
}
monitor:
if (self->config_file_monitor != NULL) { // If we already have a file monitor for the file
return;
}
self->config_file_monitor = g_file_monitor_file(
self->config_file,
G_FILE_MONITOR_NONE,
NULL,
NULL
);
g_signal_connect(self->config_file_monitor, "changed", G_CALLBACK(koto_config_monitor_handle_changed), self); // Monitor changes to our config file
if (!config_file_exists) { // File did not originally exist
koto_config_save(self); // Save immediately
}
}
void koto_config_load_libs(KotoConfig * self) {
gchar * home_dir = g_strdup(g_get_home_dir()); // Get the home directory
if (!self->has_type_audiobook) { // If we do not have a KotoLibrary for Audiobooks
gchar * audiobooks_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Audiobooks", NULL);
koto_utils_mkdir(audiobooks_path); // Create the directory just in case
KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_AUDIOBOOK, NULL, audiobooks_path); // Audiobooks relative to home directory
if (KOTO_IS_LIBRARY(lib)) { // Created built-in audiobooks lib successfully
koto_cartographer_add_library(koto_maps, lib);
koto_config_save(config);
koto_library_index(lib); // Index this library
}
g_free(audiobooks_path);
}
if (!self->has_type_music) { // If we do not have a KotoLibrary for Music
KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_MUSIC, NULL, g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); // Create a library using the user's MUSIC directory defined
if (KOTO_IS_LIBRARY(lib)) { // Created built-in music lib successfully
koto_cartographer_add_library(koto_maps, lib);
koto_config_save(config);
koto_library_index(lib); // Index this library
}
}
if (!self->has_type_podcast) { // If we do not have a KotoLibrary for Podcasts
gchar * podcasts_path = g_build_path(G_DIR_SEPARATOR_S, home_dir, "Podcasts", NULL);
koto_utils_mkdir(podcasts_path); // Create the directory just in case
KotoLibrary * lib = koto_library_new(KOTO_LIBRARY_TYPE_PODCAST, NULL, podcasts_path); // Podcasts relative to home dir
if (KOTO_IS_LIBRARY(lib)) { // Created built-in podcasts lib successfully
koto_cartographer_add_library(koto_maps, lib);
koto_config_save(config);
koto_library_index(lib); // Index this library
}
g_free(podcasts_path);
}
g_free(home_dir);
g_thread_exit(0);
}
void koto_config_monitor_handle_changed(
GFileMonitor * monitor,
GFile * file,
GFile * other_file,
GFileMonitorEvent ev,
gpointer user_data
) {
(void) monitor;
(void) file;
(void) other_file;
KotoConfig * config = user_data;
if (
(ev == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED) || // Attributes changed
(ev == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) // Changes done
) {
koto_config_refresh(config); // Refresh the config
}
}
KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self) {
return self->preferred_album_sort_type;
}
/**
* Refresh will handle any FS notify change on our Koto config file and call load
**/
void koto_config_refresh(KotoConfig * self) {
koto_config_load(self, self->path);
}
/**
* Save will write our config back out
**/
void koto_config_save(KotoConfig * self) {
GStrvBuilder * root_builder = g_strv_builder_new(); // Create a new strv builder
/* Iterate over our libraries */
GList * libs = koto_cartographer_get_libraries(koto_maps); // Get our libraries
GList * current_libs;
for (current_libs = libs; current_libs != NULL; current_libs = current_libs->next) { // Iterate over our libraries
KotoLibrary * lib = current_libs->data;
gchar * lib_config = koto_library_to_config_string(lib); // Get the config string
g_strv_builder_add(root_builder, lib_config); // Add the config to our string builder
g_free(lib_config);
}
GParamSpec ** props_list = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), NULL); // Get the propreties associated with our settings
GHashTable * sections_to_prop_keys = g_hash_table_new(g_str_hash, g_str_equal); // Create our section to hold our various sections based on props
/* Section Hashes*/
gchar * playback_hash = g_strdup("playback");
gchar * ui_hash = g_strdup("ui");
gdouble current_playback_volume = 1.0;
if (KOTO_IS_PLAYBACK_ENGINE(playback_engine)) { // Have a playback engine (useful since it may not be initialized before the config performs saving on first application load)
current_playback_volume = koto_playback_engine_get_volume(playback_engine); // Get the last used volume in the playback engine
}
self->playback_last_used_volume = current_playback_volume; // Update our value so we have it during save
int i;
for (i = 0; i < N_PROPS; i++) { // For each property
GParamSpec * spec = props_list[i]; // Get the prop
if (!G_IS_PARAM_SPEC(spec)) { // Not a spec
continue; // Skip
}
const gchar * prop_name = g_param_spec_get_name(spec);
gpointer respective_prop = NULL;
if (g_str_has_prefix(prop_name, "playback")) { // Is playback
respective_prop = playback_hash;
} else if (g_str_has_prefix(prop_name, "ui")) { // Is UI
respective_prop = ui_hash;
}
if (respective_prop == NULL) { // No property
continue;
}
GList * keys;
if (g_hash_table_contains(sections_to_prop_keys, respective_prop)) { // Already has list
keys = g_hash_table_lookup(sections_to_prop_keys, respective_prop); // Get the list
} else { // Don't have list
keys = NULL;
}
keys = g_list_append(keys, g_strdup(prop_name)); // Add the name in full
g_hash_table_insert(sections_to_prop_keys, respective_prop, keys); // Replace list (or add it)
}
GHashTableIter iter;
gpointer section_name, section_props;
g_hash_table_iter_init(&iter, sections_to_prop_keys);
while (g_hash_table_iter_next(&iter, &section_name, &section_props)) {
GStrvBuilder * section_builder = g_strv_builder_new(); // Make our string builder
g_strv_builder_add(section_builder, g_strdup_printf("[%s]", (gchar*) section_name)); // Add section as [section]
GList * current_section_keyname;
for (current_section_keyname = section_props; current_section_keyname != NULL; current_section_keyname = current_section_keyname->next) { // Iterate over property names
GValue prop_val_raw = G_VALUE_INIT; // Initialize our GValue
g_object_get_property(G_OBJECT(self), current_section_keyname->data, &prop_val_raw);
gchar * prop_val = g_strdup_value_contents(&prop_val_raw);
if ((g_strcmp0(prop_val, "TRUE") == 0) || (g_strcmp0(prop_val, "FALSE") == 0)) { // TRUE or FALSE from a boolean type
prop_val = g_utf8_strdown(prop_val, -1); // Change it to be lowercased
}
gchar * key_name = g_strdup(current_section_keyname->data);
gchar * key_name_replaced = koto_utils_string_replace_all(key_name, g_strdup_printf("%s-", (gchar*) section_name), ""); // Remove SECTIONNAME-
const gchar * line = g_strdup_printf("\t%s = %s", key_name_replaced, prop_val);
g_strv_builder_add(section_builder, line); // Add the line
g_free(key_name_replaced);
g_free(key_name);
}
GStrv lines = g_strv_builder_end(section_builder); // Get all the lines as a GStrv which is a gchar **
gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline
g_strfreev(lines); // Free our lines
g_strv_builder_add(root_builder, content); // Add section content to root builder
g_strv_builder_unref(section_builder); // Unref our builder
}
g_hash_table_unref(sections_to_prop_keys); // Free our hash table
GStrv lines = g_strv_builder_end(root_builder); // Get all the lines as a GStrv which is a gchar **
gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline
g_strfreev(lines); // Free our lines
g_strv_builder_unref(root_builder); // Unref our root builder
ulong file_content_length = g_utf8_strlen(content, -1);
g_file_replace_contents(
self->config_file,
content,
file_content_length,
NULL,
FALSE,
G_FILE_CREATE_PRIVATE,
NULL,
NULL,
NULL
);
}
KotoConfig * koto_config_new() {
return g_object_new(KOTO_TYPE_CONFIG, NULL);
}

View file

@ -1,55 +0,0 @@
/* config.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib.h>
#include <glib-2.0/gio/gio.h>
#include "../indexer/misc-types.h"
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_CONFIG (koto_config_get_type())
G_DECLARE_FINAL_TYPE(KotoConfig, koto_config, KOTO, CONFIG, GObject)
KotoConfig* koto_config_new();
void koto_config_load(
KotoConfig * self,
gchar * path
);
void koto_config_load_libs(KotoConfig * self);
void koto_config_monitor_handle_changed(
GFileMonitor * monitor,
GFile * file,
GFile * other_file,
GFileMonitorEvent ev,
gpointer user_data
);
KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self);
void koto_config_refresh(KotoConfig * self);
void koto_config_save(KotoConfig * self);
G_END_DECLS

View file

@ -1,910 +0,0 @@
/* cartographer.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include "../koto-utils.h"
#include "cartographer.h"
enum {
SIGNAL_ALBUM_ADDED,
SIGNAL_ALBUM_REMOVED,
SIGNAL_ARTIST_ADDED,
SIGNAL_ARTIST_REMOVED,
SIGNAL_LIBRARY_ADDED,
SIGNAL_LIBRARY_REMOVED,
SIGNAL_PLAYLIST_ADDED,
SIGNAL_PLAYLIST_REMOVED,
SIGNAL_TRACK_ADDED,
SIGNAL_TRACK_REMOVED,
N_SIGNALS
};
static guint cartographer_signals[N_SIGNALS] = {
0
};
struct _KotoCartographer {
GObject parent_instance;
GHashTable * albums;
GHashTable * artists;
GHashTable * artists_name_to_uuid;
GHashTable * libraries;
GHashTable * playlists;
GHashTable * tracks;
GHashTable * tracks_by_uniqueish_key;
};
struct _KotoCartographerClass {
GObjectClass parent_class;
void (* album_added) (
KotoCartographer * cartographer,
KotoAlbum * album
);
void (* album_removed) (
KotoCartographer * cartographer,
KotoAlbum * album
);
void (* artist_added) (
KotoCartographer * cartographer,
KotoArtist * artist
);
void (* artist_removed) (
KotoCartographer * cartographer,
KotoArtist * artist
);
void (* library_added) (
KotoCartographer * cartographer,
KotoLibrary * library
);
void (* library_removed) (
KotoCartographer * cartographer,
KotoLibrary * library
);
void (* playlist_added) (
KotoCartographer * cartographer,
KotoPlaylist * playlist
);
void (* playlist_removed) (
KotoCartographer * cartographer,
KotoPlaylist * playlist
);
void (* track_added) (
KotoCartographer * cartographer,
KotoTrack * track
);
void (* track_removed) (
KotoCartographer * cartographer,
KotoTrack * track
);
};
G_DEFINE_TYPE(KotoCartographer, koto_cartographer, G_TYPE_OBJECT);
KotoCartographer * koto_maps = NULL;
static void koto_cartographer_class_init(KotoCartographerClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
cartographer_signals[SIGNAL_ALBUM_ADDED] = g_signal_new(
"album-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, album_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_ALBUM
);
cartographer_signals[SIGNAL_ALBUM_REMOVED] = g_signal_new(
"album-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, album_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_ARTIST_ADDED] = g_signal_new(
"artist-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, artist_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_ARTIST
);
cartographer_signals[SIGNAL_ARTIST_REMOVED] = g_signal_new(
"artist-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, artist_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
2,
G_TYPE_CHAR,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_LIBRARY_ADDED] = g_signal_new(
"library-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, library_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_LIBRARY
);
cartographer_signals[SIGNAL_LIBRARY_ADDED] = g_signal_new(
"library-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, library_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_LIBRARY
);
cartographer_signals[SIGNAL_PLAYLIST_ADDED] = g_signal_new(
"playlist-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, playlist_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_PLAYLIST
);
cartographer_signals[SIGNAL_PLAYLIST_REMOVED] = g_signal_new(
"playlist-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, playlist_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_TRACK_ADDED] = g_signal_new(
"track-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, track_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_TRACK
);
cartographer_signals[SIGNAL_TRACK_REMOVED] = g_signal_new(
"track-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, track_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
}
static void koto_cartographer_init(KotoCartographer * self) {
self->albums = g_hash_table_new(g_str_hash, g_str_equal);
self->artists = g_hash_table_new(g_str_hash, g_str_equal);
self->artists_name_to_uuid = g_hash_table_new(g_str_hash, g_str_equal);
self->libraries = g_hash_table_new(g_str_hash, g_str_equal);
self->playlists = g_hash_table_new(g_str_hash, g_str_equal);
self->tracks = g_hash_table_new(g_str_hash, g_str_equal);
self->tracks_by_uniqueish_key = g_hash_table_new(g_str_hash, g_str_equal);
}
void koto_cartographer_add_album(
KotoCartographer * self,
KotoAlbum * album
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_ALBUM(album)) {
return;
}
gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID
if (!koto_utils_string_is_valid(album_uuid) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID
return;
}
g_hash_table_replace(self->albums, album_uuid, album);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ALBUM_ADDED],
0,
album
);
}
void koto_cartographer_add_artist(
KotoCartographer * self,
KotoArtist * artist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_ARTIST(artist)) { // Not an artist
return;
}
gchar * artist_uuid = koto_artist_get_uuid(artist);
if (!koto_utils_string_is_valid(artist_uuid) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID
return;
}
g_hash_table_replace(self->artists_name_to_uuid, koto_artist_get_name(artist), artist_uuid); // Add the UUID as a value with the key being the name of the artist
g_hash_table_replace(self->artists, artist_uuid, artist);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ARTIST_ADDED],
0,
artist
);
}
void koto_cartographer_add_library(
KotoCartographer * self,
KotoLibrary * library
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_LIBRARY(library)) { // Not a library
return;
}
gchar * library_uuid = koto_library_get_uuid(library);
if (!koto_utils_string_is_valid(library_uuid) || koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID
return;
}
g_hash_table_replace(self->libraries, library_uuid, library); // Add the library
g_signal_emit(
// Emit our library added signal
self,
cartographer_signals[SIGNAL_LIBRARY_ADDED],
0,
library
);
}
void koto_cartographer_add_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
return;
}
gchar * playlist_uuid = koto_playlist_get_uuid(playlist);
if (!koto_playlist_get_uuid(playlist) || koto_cartographer_has_playlist_by_uuid(self, playlist_uuid)) { // Have the playlist or invalid UUID
return;
}
g_hash_table_replace(self->playlists, playlist_uuid, playlist);
if (koto_playlist_get_is_finalized(playlist)) { // Already finalized
koto_cartographer_emit_playlist_added(playlist, self); // Emit playlist-added immediately
} else { // Not finalized
g_signal_connect(playlist, "track-load-finalized", G_CALLBACK(koto_cartographer_emit_playlist_added), self);
}
}
void koto_cartographer_emit_playlist_added(
KotoPlaylist * playlist,
KotoCartographer * self
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
g_signal_emit(
self,
cartographer_signals[SIGNAL_PLAYLIST_ADDED],
0,
playlist
);
}
void koto_cartographer_add_track(
KotoCartographer * self,
KotoTrack * track
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
gchar * track_uuid = koto_track_get_uuid(track);
if (!koto_utils_string_is_valid(track_uuid) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID
return;
}
g_hash_table_replace(self->tracks, track_uuid, track);
g_signal_emit(
self,
cartographer_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
KotoAlbum * koto_cartographer_get_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
return g_hash_table_lookup(self->albums, album_uuid);
}
GHashTable * koto_cartographer_get_artists(KotoCartographer * self) {
return self->artists;
}
KotoArtist * koto_cartographer_get_artist_by_name(
KotoCartographer * self,
gchar * artist_name
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(artist_name)) { // Not a valid name
return NULL;
}
return koto_cartographer_get_artist_by_uuid(self, g_hash_table_lookup(self->artists_name_to_uuid, artist_name));
}
KotoArtist * koto_cartographer_get_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(artist_uuid)) {
return NULL;
}
return g_hash_table_lookup(self->artists, artist_uuid);
}
KotoLibrary * koto_cartographer_get_library_by_uuid(
KotoCartographer * self,
gchar * library_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid string
return NULL;
}
return g_hash_table_lookup(self->libraries, library_uuid);
}
KotoLibrary * koto_cartographer_get_library_containing_path(
KotoCartographer * self,
gchar * relative_path
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(relative_path)) { // Not a valid string
return NULL;
}
GList * libs = koto_cartographer_get_libraries(self); // Get all the libraries, sorted based on priority
GList * current;
for (current = libs; current != NULL; current = current->next) { // For each library
KotoLibrary * lib = (KotoLibrary*) current->data;
gchar * lib_path = koto_library_get_path(lib); // Get the path for the library
GFile * track_file = g_file_new_build_filename(lib_path, relative_path, NULL); // Build a path from storage to file
if (g_file_query_exists(track_file, NULL)) { // If this library contains this file
g_object_unref(track_file);
return lib;
}
g_object_unref(track_file);
}
return NULL;
}
GList * koto_cartographer_get_libraries_for_storage_uuid(
KotoCartographer * self,
gchar * storage_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
GList * libraries = NULL; // Initialize our list
if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid storage UUID string
return libraries;
}
// TODO: Implement koto_cartographer_get_libraries_for_storage_uuid
return libraries;
}
GList * koto_cartographer_get_libraries(KotoCartographer * self) {
GList * libraries = g_hash_table_get_values(self->libraries);
// TODO: Implement priority mechanism
return libraries;
}
GHashTable * koto_cartographer_get_playlists(KotoCartographer * self) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
return self->playlists;
}
KotoPlaylist * koto_cartographer_get_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
return g_hash_table_lookup(self->playlists, playlist_uuid);
}
KotoTrack * koto_cartographer_get_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(track_uuid)) {
return NULL;
}
return g_hash_table_lookup(self->tracks, track_uuid);
}
KotoTrack * koto_cartographer_get_track_by_uniqueish_key(
KotoCartographer * self,
gchar * key
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return NULL;
}
if (!koto_utils_string_is_valid(key)) {
return NULL;
}
return g_hash_table_lookup(self->tracks_by_uniqueish_key, key);
}
gboolean koto_cartographer_has_album(
KotoCartographer * self,
KotoAlbum * album
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!KOTO_IS_ALBUM(album)) {
return FALSE;
}
gchar * album_uuid = NULL;
g_object_get(album, "uuid", &album_uuid, NULL);
return koto_cartographer_has_album_by_uuid(self, album_uuid);
}
gboolean koto_cartographer_has_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!koto_utils_string_is_valid(album_uuid)) { // Not a valid UUID
return FALSE;
}
return g_hash_table_contains(self->albums, album_uuid);
}
gboolean koto_cartographer_has_artist(
KotoCartographer * self,
KotoArtist * artist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!KOTO_IS_ARTIST(artist)) {
return FALSE;
}
return koto_cartographer_has_artist_by_uuid(self, koto_artist_get_uuid(artist));
}
gboolean koto_cartographer_has_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!koto_utils_string_is_valid(artist_uuid)) { // Not a valid UUID
return FALSE;
}
return g_hash_table_contains(self->artists, artist_uuid);
}
gboolean koto_cartographer_has_library(
KotoCartographer * self,
KotoLibrary * library
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!KOTO_IS_LIBRARY(library)) {
return FALSE;
}
// TODO: return koto_cartographer_has_library_by_uuid(self, koto_library_get_uuid(library)) -- Need to implement get uuid func
return FALSE;
}
gboolean koto_cartographer_has_library_by_uuid(
KotoCartographer * self,
gchar * library_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid UUID
return FALSE;
}
return g_hash_table_contains(self->libraries, library_uuid);
}
gboolean koto_cartographer_has_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
return FALSE;
}
return koto_cartographer_has_playlist_by_uuid(self, koto_playlist_get_uuid(playlist));
}
gboolean koto_cartographer_has_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid UUID
return FALSE;
}
return g_hash_table_contains(self->playlists, playlist_uuid);
}
gboolean koto_cartographer_has_track(
KotoCartographer * self,
KotoTrack * track
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return FALSE;
}
return koto_cartographer_has_album_by_uuid(self, koto_track_get_uuid(track));
}
gboolean koto_cartographer_has_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return FALSE;
}
if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid UUID
return FALSE;
}
return g_hash_table_contains(self->tracks, track_uuid);
}
void koto_cartographer_remove_album(
KotoCartographer * self,
KotoAlbum * album
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_ALBUM(album)) { // Not an album
return;
}
koto_cartographer_remove_album_by_uuid(self, koto_album_get_uuid(album));
}
void koto_cartographer_remove_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!koto_utils_string_is_valid(album_uuid)) {
return;
}
if (!g_hash_table_contains(self->albums, album_uuid)) { // Album does not exist in albums
return;
}
g_hash_table_remove(self->albums, album_uuid);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ALBUM_REMOVED],
0,
album_uuid
);
}
void koto_cartographer_remove_artist(
KotoCartographer * self,
KotoArtist * artist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_ARTIST(artist)) { // Not an artist
return;
}
gchar * artist_uuid = koto_artist_get_uuid(artist);
gchar * artist_name = koto_artist_get_name(artist);
g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist
g_hash_table_remove(self->artists, artist_uuid);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ARTIST_REMOVED],
0,
artist_uuid,
artist_name
);
}
void koto_cartographer_remove_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!koto_utils_string_is_valid(artist_uuid)) { // Artist UUID not valid
return;
}
if (!g_hash_table_contains(self->artists, artist_uuid)) { // Not in hash table
return;
}
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(self, artist_uuid);
if (!KOTO_IS_ARTIST(artist)) {
return;
}
gchar * artist_name = koto_artist_get_name(artist);
g_hash_table_remove(self->artists_name_to_uuid, artist_name); // Add the UUID as a value with the key being the name of the artist
g_hash_table_remove(self->artists, artist_uuid);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ARTIST_REMOVED],
0,
artist_uuid,
artist_name
);
}
void koto_cartographer_remove_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_PLAYLIST(playlist)) {
return;
}
koto_cartographer_remove_playlist_by_uuid(self, koto_playlist_get_uuid(playlist));
}
void koto_cartographer_remove_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid playlist UUID string
return;
}
if (!g_hash_table_contains(self->playlists, playlist_uuid)) { // Not in hash table
return;
}
g_hash_table_remove(self->playlists, playlist_uuid);
g_signal_emit(
self,
cartographer_signals[SIGNAL_PLAYLIST_REMOVED],
0,
playlist_uuid
);
}
void koto_cartographer_remove_track(
KotoCartographer * self,
KotoTrack * track
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
koto_cartographer_remove_track_by_uuid(self, koto_track_get_uuid(track));
}
void koto_cartographer_remove_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
) {
if (!KOTO_IS_CARTOGRAPHER(self)) {
return;
}
if (!koto_utils_string_is_valid(track_uuid)) {
return;
}
if (!g_hash_table_contains(self->tracks, track_uuid)) { // Not in hash table
return;
}
g_hash_table_remove(self->tracks, track_uuid);
g_signal_emit(
self,
cartographer_signals[SIGNAL_TRACK_REMOVED],
0,
track_uuid
);
}
KotoCartographer * koto_cartographer_new() {
return g_object_new(KOTO_TYPE_CARTOGRAPHER, NULL);
}

View file

@ -1,221 +0,0 @@
/* cartographer.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include "../indexer/structs.h"
#include "../playlist/playlist.h"
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_CARTOGRAPHER koto_cartographer_get_type()
typedef struct _KotoCartographer KotoCartographer;
typedef struct _KotoCartographerClass KotoCartographerClass;
GLIB_AVAILABLE_IN_ALL
GType koto_cartographer_get_type(void) G_GNUC_CONST;
#define KOTO_IS_CARTOGRAPHER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_CARTOGRAPHER))
/**
* Cartographer Functions
**/
KotoCartographer * koto_cartographer_new();
void koto_cartographer_add_album(
KotoCartographer * self,
KotoAlbum * album
);
void koto_cartographer_add_artist(
KotoCartographer * self,
KotoArtist * artist
);
void koto_cartographer_add_library(
KotoCartographer * self,
KotoLibrary * library
);
void koto_cartographer_add_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
);
void koto_cartographer_add_track(
KotoCartographer * self,
KotoTrack * track
);
void koto_cartographer_emit_playlist_added(
KotoPlaylist * playlist,
KotoCartographer * self
);
KotoAlbum * koto_cartographer_get_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
);
GHashTable * koto_cartographer_get_artists(KotoCartographer * self);
KotoArtist * koto_cartographer_get_artist_by_name(
KotoCartographer * self,
gchar * artist_name
);
KotoArtist * koto_cartographer_get_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
);
KotoLibrary * koto_cartographer_get_library_by_uuid(
KotoCartographer * self,
gchar * library_uuid
);
KotoLibrary * koto_cartographer_get_library_containing_path(
KotoCartographer * self,
gchar * path
);
GList * koto_cartographer_get_libraries(KotoCartographer * self);
GList * koto_cartographer_get_libraries_for_storage_uuid(
KotoCartographer * self,
gchar * storage_uuid
);
KotoPlaylist * koto_cartographer_get_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
);
GHashTable * koto_cartographer_get_playlists(KotoCartographer * self);
KotoTrack * koto_cartographer_get_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
);
KotoTrack * koto_cartographer_get_track_by_uniqueish_key(
KotoCartographer * self,
gchar * key
);
gboolean koto_cartographer_has_album(
KotoCartographer * self,
KotoAlbum * album
);
gboolean koto_cartographer_has_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
);
gboolean koto_cartographer_has_artist(
KotoCartographer * self,
KotoArtist * artist
);
gboolean koto_cartographer_has_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
);
gboolean koto_cartographer_has_library(
KotoCartographer * self,
KotoLibrary * library
);
gboolean koto_cartographer_has_library_by_uuid(
KotoCartographer * self,
gchar * library_uuid
);
gboolean koto_cartographer_has_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
);
gboolean koto_cartographer_has_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
);
gboolean koto_cartographer_has_track(
KotoCartographer * self,
KotoTrack * track
);
gboolean koto_cartographer_has_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
);
void koto_cartographer_remove_album(
KotoCartographer * self,
KotoAlbum * album
);
void koto_cartographer_remove_album_by_uuid(
KotoCartographer * self,
gchar * album_uuid
);
void koto_cartographer_remove_artist(
KotoCartographer * self,
KotoArtist * artist
);
void koto_cartographer_remove_artist_by_uuid(
KotoCartographer * self,
gchar * artist_uuid
);
void koto_cartographer_remove_library(
KotoCartographer * self,
KotoLibrary * library
);
void koto_cartographer_remove_playlist(
KotoCartographer * self,
KotoPlaylist * playlist
);
void koto_cartographer_remove_playlist_by_uuid(
KotoCartographer * self,
gchar * playlist_uuid
);
void koto_cartographer_remove_track(
KotoCartographer * self,
KotoTrack * track
);
void koto_cartographer_remove_track_by_uuid(
KotoCartographer * self,
gchar * track_uuid
);
G_END_DECLS

View file

@ -1,110 +0,0 @@
/* db.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <sqlite3.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "db.h"
#include "../koto-paths.h"
extern gchar * koto_path_to_db;
int KOTO_DB_SUCCESS = 0;
int KOTO_DB_NEW = 1;
int KOTO_DB_FAIL = 2;
sqlite3 * koto_db = NULL;
gchar * db_filepath = NULL;
gboolean created_new_db = FALSE;
void close_db() {
sqlite3_close(koto_db);
}
int create_db_tables() {
gchar * tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);"
"CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, genres string, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES albums(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_id) FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int, album_id string, track_id string, playback_position_of_track int);"
"CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);";
return (new_transaction(tables_creation_queries, "Failed to create required tables", TRUE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL;
}
int enable_foreign_keys() {
gchar * commit_op = g_strdup("PRAGMA foreign_keys = ON;");
const gchar * transaction_err_msg = "Failed to enable foreign key support. Ensure your sqlite3 is compiled with neither SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER defined";
return (new_transaction(commit_op, transaction_err_msg, FALSE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL;
}
int have_existing_db() {
struct stat db_stat;
int success = stat(koto_path_to_db, &db_stat);
return ((success == 0) && S_ISREG(db_stat.st_mode)) ? 0 : 1;
}
int new_transaction(
gchar * operation,
const gchar * transaction_err_msg,
gboolean fatal
) {
gchar * commit_op_errmsg = NULL;
int rc = sqlite3_exec(koto_db, operation, 0, 0, &commit_op_errmsg);
if (rc != SQLITE_OK) {
(fatal) ? g_critical("%s: %s", transaction_err_msg, commit_op_errmsg) : g_warning("%s: %s", transaction_err_msg, commit_op_errmsg);
}
if (commit_op_errmsg == NULL) {
g_free(commit_op_errmsg);
}
return rc;
}
int open_db() {
int ret = KOTO_DB_SUCCESS; // Default to last return being SUCCESS
if (have_existing_db() == 1) { // If we do not have an existing DB
ret = KOTO_DB_NEW;
}
if (sqlite3_open(koto_path_to_db, &koto_db) != KOTO_DB_SUCCESS) { // If we failed to open the database file
g_critical("Failed to open or create database: %s", sqlite3_errmsg(koto_db));
return KOTO_DB_FAIL;
}
if (enable_foreign_keys() != KOTO_DB_SUCCESS) { // If we failed to enable foreign keys
return KOTO_DB_FAIL;
}
if (create_db_tables() != KOTO_DB_SUCCESS) { // Failed to create our database tables
return KOTO_DB_FAIL;
}
if (ret == KOTO_DB_NEW) {
created_new_db = TRUE;
}
return ret;
}

View file

@ -1,41 +0,0 @@
/* db.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <sqlite3.h>
extern int KOTO_DB_SUCCESS;
extern int KOTO_DB_NEW;
extern int KOTO_DB_FAIL;
void close_db();
int create_db_tables();
gchar * get_db_path();
int enable_foreign_keys();
int have_existing_db();
int new_transaction(
gchar * operation,
const gchar * transaction_err_msg,
gboolean fatal
);
int open_db();

View file

@ -1,392 +0,0 @@
/* loaders.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cartographer.h"
#include "db.h"
#include "loaders.h"
#include "../indexer/album-playlist-funcs.h"
#include "../indexer/structs.h"
#include "../koto-utils.h"
extern KotoCartographer * koto_maps;
extern sqlite3 * koto_db;
int process_artists(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) data;
(void) num_columns;
(void) column_names; // Don't need any of the params
gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[0])); // First column is UUID
gchar * artist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is artist name
KotoArtist * artist = koto_artist_new_with_uuid(artist_uuid); // Create our artist with the UUID
g_object_set(
artist,
"name",
artist_name, // Set name
NULL);
int artist_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM libraries_artists WHERE artist_id=\"%s\"", artist_uuid), process_artist_paths, artist, NULL); // Process all the paths for this given artist
if (artist_paths != SQLITE_OK) { // Failed to get our artists_paths
g_critical("Failed to read our paths for this artist: %s", sqlite3_errmsg(koto_db));
return 1;
}
koto_cartographer_add_artist(koto_maps, artist); // Add the artist to our global cartographer
int albums_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM albums WHERE artist_id=\"%s\"", artist_uuid), process_albums, artist, NULL); // Process our albums
if (albums_rc != SQLITE_OK) { // Failed to get our albums
g_critical("Failed to read our albums: %s", sqlite3_errmsg(koto_db));
return 1;
}
koto_artist_set_as_finalized(artist); // Indicate it is finalized
int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE artist_id=\"%s\" AND album_id=''", artist_uuid), process_tracks, NULL, NULL); // Load all tracks for an artist that are NOT in an album (e.g. artists without albums)
if (tracks_rc != SQLITE_OK) { // Failed to get our tracks
g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db));
return 1;
}
g_free(artist_uuid);
g_free(artist_name);
return 0;
}
int process_artist_paths(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) num_columns;
(void) column_names; // Don't need these
KotoArtist * artist = (KotoArtist*) data;
gchar * library_uuid = g_strdup(koto_utils_string_unquote(fields[0]));
gchar * relative_path = g_strdup(koto_utils_string_unquote(fields[2]));
KotoLibrary * lib = koto_cartographer_get_library_by_uuid(koto_maps, library_uuid); // Get the library for this artist
if (!KOTO_IS_LIBRARY(lib)) { // Failed to get the library for this UUID
return 0;
}
koto_artist_set_path(artist, lib, relative_path, FALSE); // Add the relative path from the db for this artist and lib to the Artist, do not commit
return 0;
}
int process_albums(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) num_columns;
(void) column_names; // Don't need these
KotoArtist * artist = (KotoArtist*) data;
gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[0]));
gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1]));
gchar * album_name = g_strdup(koto_utils_string_unquote(fields[2]));
gchar * album_description = (fields[3] != NULL) ? g_strdup(koto_utils_string_unquote(fields[3])) : NULL;
gchar * album_narrator = (fields[4] != NULL) ? g_strdup(koto_utils_string_unquote(fields[4])) : NULL;
gchar * album_art = (fields[5] != NULL) ? g_strdup(koto_utils_string_unquote(fields[5])) : NULL;
gchar * album_genres = (fields[6] != NULL) ? g_strdup(koto_utils_string_unquote(fields[6])) : NULL;
guint64 * album_year = (guint64*) g_ascii_strtoull(fields[7], NULL, 10);
KotoAlbum * album = koto_album_new_with_uuid(artist, album_uuid); // Create our album
g_object_set(
album,
"name",
album_name, // Set name
"description",
album_description,
"narrator",
album_narrator,
"art-path",
album_art, // Set art path if any
"preparsed-genres",
album_genres,
"year",
album_year,
NULL
);
koto_cartographer_add_album(koto_maps, album); // Add the album to our global cartographer
koto_artist_add_album(artist, album); // Add the album
int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE album_id=\"%s\"", album_uuid), process_tracks, NULL, NULL); // Process all the tracks for this specific album
if (tracks_rc != SQLITE_OK) { // Failed to get our tracks
g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db));
return 1;
}
koto_album_mark_as_finalized(album); // Mark the album as finalized now that all tracks have been loaded, allowing our internal album playlist to re-sort itself
g_free(album_uuid);
g_free(artist_uuid);
g_free(album_name);
g_free(album_genres);
if (album_art != NULL) {
g_free(album_art);
}
return 0;
}
int process_playlists(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) data;
(void) num_columns;
(void) column_names; // Don't need any of the params
gchar * playlist_uuid = g_strdup(koto_utils_string_unquote(fields[0])); // First column is UUID
gchar * playlist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is playlist name
gchar * playlist_art_path = g_strdup(koto_utils_string_unquote(fields[2])); // Third column is any art path
guint64 playlist_preferred_sort = g_ascii_strtoull(koto_utils_string_unquote(fields[3]), NULL, 10); // Fourth column is preferred model which is an int
gchar * playlist_album_id = g_strdup(koto_utils_string_unquote(fields[4])); // Fifth column is any album ID
gchar * playlist_current_track_id = g_strdup(koto_utils_string_unquote(fields[5])); // Sixth column is any track ID
guint64 playlist_track_current_playback_pos = g_ascii_strtoull(koto_utils_string_unquote(fields[6]), NULL, 10); // Seventh column is any playback position for the track
KotoPreferredPlaylistSortType sort_type = (KotoPreferredPlaylistSortType) playlist_preferred_sort;
KotoPlaylist * playlist = NULL;
gboolean for_album = FALSE;
if (koto_utils_string_is_valid(playlist_album_id)) { // Album UUID is set
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, playlist_album_id); // Get the album based on the album ID set for the playlist
if (KOTO_IS_ALBUM(album)) { // Is an album
playlist = koto_album_get_playlist(album); // Get the playlist
for_album = TRUE;
}
} else {
playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID
koto_playlist_apply_model(playlist, sort_type); // Set the model based on what is stored
koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer
}
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist yet, in this scenario it is likely the album no longer exists
goto free;
}
g_object_set(
playlist,
"name",
playlist_name,
"art-path",
playlist_art_path,
"ephemeral",
for_album,
NULL
);
if (!for_album) { // Isn't for an album
int playlist_tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM playlist_tracks WHERE playlist_id=\"%s\" ORDER BY position ASC", playlist_uuid), process_playlists_tracks, playlist, NULL); // Process our playlist tracks
if (playlist_tracks_rc != SQLITE_OK) { // Failed to get our playlist tracks
g_critical("Failed to read our playlist tracks: %s", sqlite3_errmsg(koto_db));
goto free;
}
}
if (koto_utils_string_is_valid(playlist_current_track_id)) { // If we have a track UUID (probably)
KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, playlist_current_track_id); // Get the track UUID
if (KOTO_IS_TRACK(track)) { // If this is a track
koto_track_set_playback_position(track, playlist_track_current_playback_pos); // Set the playback position of the track
koto_playlist_set_track_as_current(playlist, playlist_current_track_id); // Ensure we have this track set as the current one in the playlist
}
}
koto_playlist_mark_as_finalized(playlist); // Mark as finalized since loading should be complete
free:
g_free(playlist_uuid);
g_free(playlist_name);
g_free(playlist_art_path);
return 0;
}
int process_playlists_tracks(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) data;
(void) num_columns;
(void) column_names; // Don't need these
gchar * playlist_uuid = g_strdup(koto_utils_string_unquote(fields[1]));
gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[2]));
KotoPlaylist * playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist
KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track
if (!KOTO_IS_PLAYLIST(playlist)) {
goto freeforret;
}
koto_playlist_add_track(playlist, track, FALSE, FALSE); // Add the track to the playlist but don't re-commit to the table
freeforret:
g_free(playlist_uuid);
g_free(track_uuid);
return 0;
}
int process_tracks(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
(void) data;
(void) num_columns;
(void) column_names; // Don't need these
gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[0]));
KotoTrack * existing_track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid);
if (KOTO_IS_TRACK(existing_track)) { // Already have track
g_free(track_uuid);
return 0;
}
gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1]));
gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[2]));
gchar * name = g_strdup(koto_utils_string_unquote(fields[3]));
guint * disc_num = (guint*) g_ascii_strtoull(fields[4], NULL, 10);
guint64 * position = (guint64*) g_ascii_strtoull(fields[5], NULL, 10);
guint64 * duration = (guint64*) g_ascii_strtoull(fields[6], NULL, 10);
gchar * genres = g_strdup(koto_utils_string_unquote(fields[7]));
KotoTrack * track = koto_track_new_with_uuid(track_uuid); // Create our file
g_object_set(
track,
"artist-uuid",
artist_uuid,
"album-uuid",
album_uuid,
"parsed-name",
name,
"cd",
disc_num,
"position",
position,
"duration",
duration,
"preparsed-genres",
genres,
NULL
);
g_free(name);
int track_paths = sqlite3_exec(koto_db, g_strdup_printf("SELECT id, path FROM libraries_tracks WHERE track_id=\"%s\"", track_uuid), process_track_paths, track, NULL); // Process all pathes associated with the track
if (track_paths != SQLITE_OK) { // Failed to read the paths
g_warning("Failed to read paths associated with track %s: %s", track_uuid, sqlite3_errmsg(koto_db));
g_free(track_uuid);
g_free(artist_uuid);
g_free(album_uuid);
return 1;
}
koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist
koto_artist_add_track(artist, track); // Add the track for the artist
if (koto_utils_string_is_valid(album_uuid)) { // If we have an album UUID
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Attempt to get album
if (KOTO_IS_ALBUM(album)) { // This is an album
koto_album_add_track(album, track); // Add the track
}
}
g_free(track_uuid);
g_free(artist_uuid);
g_free(album_uuid);
return 0;
}
int process_track_paths(
void * data,
int num_columns,
char ** fields,
char ** column_names
) {
KotoTrack * track = (KotoTrack*) data;
(void) num_columns;
(void) column_names; // Don't need these
KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, koto_utils_string_unquote(fields[0]));
if (!KOTO_IS_LIBRARY(library)) { // Not a library
return 1;
}
koto_track_set_path(track, library, koto_utils_string_unquote(fields[1]));
return 0;
}
void read_from_db() {
int artists_rc = sqlite3_exec(koto_db, "SELECT * FROM artists", process_artists, NULL, NULL); // Process our artists
if (artists_rc != SQLITE_OK) { // Failed to get our artists
g_critical("Failed to read our artists: %s", sqlite3_errmsg(koto_db));
return;
}
int playlist_rc = sqlite3_exec(koto_db, "SELECT * FROM playlist_meta", process_playlists, NULL, NULL); // Process our playlists
if (playlist_rc != SQLITE_OK) { // Failed to get our playlists
g_critical("Failed to read our playlists: %s", sqlite3_errmsg(koto_db));
return;
}
}

View file

@ -1,67 +0,0 @@
/* loaders.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
int process_artists(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_artist_paths(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_albums(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_playlists(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_playlists_tracks(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_tracks(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
int process_track_paths(
void * data,
int num_columns,
char ** fields,
char ** column_names
);
void read_from_db();

View file

@ -1,25 +0,0 @@
/* album-playlist-funcs.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "../playlist/playlist.h"
#include "structs.h"
G_BEGIN_DECLS
KotoPlaylist * koto_album_get_playlist(KotoAlbum * self);
G_END_DECLS

View file

@ -1,851 +0,0 @@
/* album.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <magic.h>
#include <sqlite3.h>
#include <stdio.h>
#include "../db/cartographer.h"
#include "../db/db.h"
#include "../playlist/current.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "album-playlist-funcs.h"
#include "track-helpers.h"
extern KotoCartographer * koto_maps;
extern KotoCurrentPlaylist * current_playlist;
extern magic_t magic_cookie;
extern sqlite3 * koto_db;
enum {
PROP_0,
PROP_UUID,
PROP_DO_INITIAL_INDEX,
PROP_NAME,
PROP_ART_PATH,
PROP_ARTIST_UUID,
PROP_ALBUM_PREPARED_GENRES,
PROP_DESCRIPTION,
PROP_NARRATOR,
PROP_YEAR,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
enum {
SIGNAL_TRACK_ADDED,
SIGNAL_TRACK_REMOVED,
N_SIGNALS
};
static guint album_signals[N_SIGNALS] = {
0
};
struct _KotoAlbum {
GObject parent_instance;
gchar * uuid;
gchar * name;
guint64 year;
gchar * description;
gchar * narrator;
gchar * art_path;
gchar * artist_uuid;
GList * genres;
KotoPlaylist * playlist;
GHashTable * paths;
gboolean has_album_art;
gboolean do_initial_index;
gboolean finalized;
};
struct _KotoAlbumClass {
GObjectClass parent_class;
void (* track_added) (
KotoAlbum * album,
KotoTrack * track
);
void (* track_removed) (
KotoAlbum * album,
KotoTrack * track
);
};
G_DEFINE_TYPE(KotoAlbum, koto_album, G_TYPE_OBJECT);
static void koto_album_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_album_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_album_class_init(KotoAlbumClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_album_set_property;
gobject_class->get_property = koto_album_get_property;
props[PROP_UUID] = g_param_spec_string(
"uuid",
"UUID to Album in database",
"UUID to Album in database",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_DO_INITIAL_INDEX] = g_param_spec_boolean(
"do-initial-index",
"Do an initial indexing operating instead of pulling from the database",
"Do an initial indexing operating instead of pulling from the database",
FALSE,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_NAME] = g_param_spec_string(
"name",
"Name",
"Name of Album",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_ART_PATH] = g_param_spec_string(
"art-path",
"Path to Artwork",
"Path to Artwork",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_ARTIST_UUID] = g_param_spec_string(
"artist-uuid",
"UUID of Artist associated with Album",
"UUID of Artist associated with Album",
NULL,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_ALBUM_PREPARED_GENRES] = g_param_spec_string(
"preparsed-genres",
"Preparsed Genres",
"Preparsed Genres",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_DESCRIPTION] = g_param_spec_string(
"description",
"Description of Album, typically for an audiobook or podcast",
"Description of Album, typically for an audiobook or podcast",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_NARRATOR] = g_param_spec_string(
"narrator",
"Narrator of audiobook",
"Narrator of audiobook",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_YEAR] = g_param_spec_uint64(
"year",
"Year",
"Year of release",
0,
G_MAXSHORT,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
album_signals[SIGNAL_TRACK_ADDED] = g_signal_new(
"track-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoAlbumClass, track_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_TRACK
);
album_signals[SIGNAL_TRACK_REMOVED] = g_signal_new(
"track-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoAlbumClass, track_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_TRACK
);
}
static void koto_album_init(KotoAlbum * self) {
self->description = NULL;
self->genres = NULL;
self->has_album_art = FALSE;
self->narrator = NULL;
self->paths = g_hash_table_new(g_str_hash, g_str_equal);
self->playlist = koto_playlist_new_with_uuid(NULL); // Create a playlist, with UUID set to NULL temporarily (until we know the album UUID)
koto_playlist_apply_model(self->playlist, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS); // Sort by track position
koto_playlist_set_album_uuid(self->playlist, self->uuid); // Set the playlist UUID to be the same as the album
g_object_set(self->playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary
koto_cartographer_add_playlist(koto_maps, self->playlist); // Add to cartographer
self->year = 0;
}
void koto_album_add_track(
KotoAlbum * self,
KotoTrack * track
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
if (koto_playlist_get_position_of_track(self->playlist, track) != -1) { // Have added it already
return;
}
koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary
GList * track_genres = koto_track_get_genres(track); // Get the genres for the track
GList * current_genre_list;
gchar * existing_genres_as_string = koto_utils_join_string_list(self->genres, ";");
for (current_genre_list = track_genres; current_genre_list != NULL; current_genre_list = current_genre_list->next) { // Iterate over each item in the track genres
gchar * track_genre = current_genre_list->data; // Get this genre
if (g_strcmp0(track_genre, "") == 0) {
continue;
}
if (koto_utils_string_contains_substring(existing_genres_as_string, track_genre)) { // Genres list contains this genre
continue;
}
self->genres = g_list_append(self->genres, g_strdup(track_genre)); // Duplicate the genre and add it to our list
}
if (self->year == 0) { // Don't have a year set yet
guint64 track_year = koto_track_get_year(track); // Get the year from this track
if (track_year > 0) { // Have a probably valid year set
self->year = track_year; // Set our track
}
}
if (!koto_utils_string_is_valid(self->narrator)) { // No narrator set yet
gchar * track_narrator = koto_track_get_narrator(track); // Get the narrator for the track
if (koto_utils_string_is_valid(track_narrator)) { // If this track has a narrator
self->narrator = g_strdup(track_narrator);
g_free(track_narrator);
}
}
koto_playlist_add_track(self->playlist, track, FALSE, FALSE); // Add the track to our internal playlist
g_signal_emit(
self,
album_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
void koto_album_commit(KotoAlbum * self) {
if (self->art_path == NULL) { // If art_path isn't defined when committing
koto_album_set_album_art(self, ""); // Set to an empty string
}
gchar * genres_string = koto_utils_join_string_list(self->genres, ";");
gchar * commit_op = g_strdup_printf(
"INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year)"
"VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), '%s', %ld)"
"ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, description=excluded.description, narrator=excluded.narrator, art_path=excluded.art_path, genres=excluded.genres, year=excluded.year;",
self->uuid,
self->artist_uuid,
koto_utils_string_get_valid(self->name),
koto_utils_string_get_valid(self->description),
koto_utils_string_get_valid(self->narrator),
koto_utils_string_get_valid(self->art_path),
koto_utils_string_get_valid(genres_string),
self->year
);
new_transaction(commit_op, "Failed to write our album to the database", FALSE);
g_free(genres_string);
GHashTableIter paths_iter;
g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths
gpointer lib_uuid_ptr, album_rel_path_ptr;
while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &album_rel_path_ptr)) {
gchar * lib_uuid = lib_uuid_ptr;
gchar * album_rel_path = album_rel_path_ptr;
gchar * commit_op = g_strdup_printf(
"INSERT INTO libraries_albums(id, album_id, path)"
"VALUES ('%s', '%s', quote(\"%s\"))"
"ON CONFLICT(id, album_id) DO UPDATE SET path=excluded.path;",
lib_uuid,
self->uuid,
album_rel_path
);
new_transaction(commit_op, "Failed to add this path for the album", FALSE);
}
}
void koto_album_find_album_art(KotoAlbum * self) {
if (self->has_album_art) { // If we already have album art
return;
}
gchar * optimal_album_path = koto_album_get_path(self);
DIR * dir = opendir(optimal_album_path); // Attempt to open our directory
if (dir == NULL) {
return;
}
struct dirent * entry;
while ((entry = readdir(dir))) {
if (entry->d_type != DT_REG) { // Not a regular file
continue; // SKIP
}
if (g_str_has_prefix(entry->d_name, ".")) { // Reference to parent dir, self, or a hidden item
continue; // Skip
}
gchar * full_path = g_strdup_printf("%s%s%s", optimal_album_path, G_DIR_SEPARATOR_S, entry->d_name);
const char * mime_type = magic_file(magic_cookie, full_path);
if (
(mime_type == NULL) || // Failed to get the mimetype
((mime_type != NULL) && !g_str_has_prefix(mime_type, "image/")) // Got the mimetype but it is not an image
) {
g_free(full_path);
continue; // Skip
}
gchar * album_art_no_ext = g_strdup(koto_utils_get_filename_without_extension(entry->d_name)); // Get the name of the file without the extension
gchar * lower_art = g_strdup(g_utf8_strdown(album_art_no_ext, -1)); // Lowercase
g_free(album_art_no_ext);
gboolean should_set = (g_strrstr(lower_art, "Small") == NULL) && (g_strrstr(lower_art, "back") == NULL); // Not back or small
g_free(lower_art);
if (should_set) {
koto_album_set_album_art(self, full_path);
g_free(full_path);
break;
}
g_free(full_path);
}
closedir(dir);
}
static void koto_album_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoAlbum * self = KOTO_ALBUM(obj);
switch (prop_id) {
case PROP_UUID:
g_value_set_string(val, self->uuid);
break;
case PROP_DO_INITIAL_INDEX:
g_value_set_boolean(val, self->do_initial_index);
break;
case PROP_NAME:
g_value_set_string(val, self->name);
break;
case PROP_ART_PATH:
g_value_set_string(val, koto_album_get_art(self));
break;
case PROP_ARTIST_UUID:
g_value_set_string(val, self->artist_uuid);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_album_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoAlbum * self = KOTO_ALBUM(obj);
switch (prop_id) {
case PROP_UUID:
koto_album_set_uuid(self, g_value_get_string(val));
break;
case PROP_DO_INITIAL_INDEX:
self->do_initial_index = g_value_get_boolean(val);
break;
case PROP_NAME: // Name of album
koto_album_set_album_name(self, g_value_get_string(val));
break;
case PROP_ART_PATH: // Path to art
koto_album_set_album_art(self, g_value_get_string(val));
break;
case PROP_ARTIST_UUID:
koto_album_set_artist_uuid(self, g_value_get_string(val));
break;
case PROP_ALBUM_PREPARED_GENRES:
koto_album_set_preparsed_genres(self, g_strdup(g_value_get_string(val)));
break;
case PROP_DESCRIPTION:
koto_album_set_description(self, g_value_get_string(val));
break;
case PROP_NARRATOR:
koto_album_set_narrator(self, g_value_get_string(val));
break;
case PROP_YEAR:
koto_album_set_year(self, g_value_get_uint64(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
gchar * koto_album_get_art(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return g_strdup("");
}
return g_strdup((self->has_album_art && koto_utils_string_is_valid(self->art_path)) ? self->art_path : "");
}
gchar * koto_album_get_artist_uuid(KotoAlbum * self) {
return KOTO_IS_ALBUM(self) ? self->artist_uuid : NULL;
}
gchar * koto_album_get_description(KotoAlbum * self) {
return KOTO_IS_ALBUM(self) ? self->description : NULL;
}
GList * koto_album_get_genres(KotoAlbum * self) {
return KOTO_IS_ALBUM(self) ? self->genres : NULL;
}
KotoLibraryType koto_album_get_lib_type(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return KOTO_LIBRARY_TYPE_UNKNOWN;
}
return koto_artist_get_lib_type(koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid)); // Get the lib type for the artist. If artist isn't valid, we just return UNKNOWN
}
gchar * koto_album_get_name(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return NULL;
}
if (!koto_utils_string_is_valid(self->name)) { // Not set
return NULL;
}
return self->name; // Return name of the album
}
gchar * koto_album_get_narrator(KotoAlbum * self) {
return KOTO_IS_ALBUM(self) ? self->narrator : NULL;
}
gchar * koto_album_get_path(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self) || (KOTO_IS_ALBUM(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not an album or is but we have no paths associated with it
return NULL;
}
GList * libs = koto_cartographer_get_libraries(koto_maps); // Get all of our libraries
GList * cur_lib_list;
for (cur_lib_list = libs; cur_lib_list != NULL; cur_lib_list = libs->next) { // Iterate over our libraries
KotoLibrary * cur_library = libs->data; // Get this as a KotoLibrary
gchar * library_relative_path = g_hash_table_lookup(self->paths, koto_library_get_uuid(cur_library)); // Get any relative path in our paths based on the current UUID
if (!koto_utils_string_is_valid(library_relative_path)) { // Not a valid path
continue;
}
return g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(cur_library), library_relative_path, NULL)); // Build our full library path using library's path and our file relative path
}
return NULL;
}
KotoPlaylist * koto_album_get_playlist(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return NULL;
}
if (!KOTO_IS_PLAYLIST(self->playlist)) { // Not a playlist
return NULL;
}
return self->playlist;
}
GListStore * koto_album_get_store(KotoAlbum * self) {
KotoPlaylist * playlist = koto_album_get_playlist(self);
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
return NULL;
}
return koto_playlist_get_store(playlist); // Return the store from the playlist
}
gchar * koto_album_get_uuid(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return NULL;
}
return self->uuid; // Return the UUID
}
guint64 koto_album_get_year(KotoAlbum * self) {
return KOTO_IS_ALBUM(self) ? self->year : 0;
}
void koto_album_mark_as_finalized(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (self->finalized) { // Already finalized
return;
}
self->finalized = TRUE;
koto_playlist_mark_as_finalized(self->playlist);
//koto_playlist_apply_model(self->playlist, self->model); // Resort our playlist
}
void koto_album_remove_track(
KotoAlbum * self,
KotoTrack * track
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
koto_playlist_remove_track_by_uuid(
self->playlist,
koto_track_get_uuid(track)
);
g_signal_emit(
self,
album_signals[SIGNAL_TRACK_REMOVED],
0,
track
);
}
void koto_album_set_album_name(
KotoAlbum * self,
const gchar * album_name
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (album_name == NULL) { // Not valid album name
return;
}
if (self->name != NULL) {
g_free(self->name);
}
self->name = g_strdup(album_name);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]);
}
void koto_album_set_artist_uuid(
KotoAlbum * self,
const gchar * artist_uuid
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (artist_uuid == NULL) {
return;
}
if (self->artist_uuid != NULL) {
g_free(self->artist_uuid);
}
self->artist_uuid = g_strdup(artist_uuid);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_UUID]);
}
void koto_album_set_album_art(
KotoAlbum * self,
const gchar * album_art
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (album_art == NULL) { // Not valid album art
return;
}
if (self->art_path != NULL) {
g_free(self->art_path);
}
self->art_path = g_strdup(album_art);
self->has_album_art = TRUE;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ART_PATH]);
}
void koto_album_set_as_current_playlist(KotoAlbum * self) {
if (!KOTO_IS_ALBUM(self)) {
return;
}
if (!KOTO_IS_CURRENT_PLAYLIST(current_playlist)) {
return;
}
if (!KOTO_IS_PLAYLIST(self->playlist)) { // Don't have a playlist for the album for some reason
return;
}
koto_current_playlist_set_playlist(current_playlist, self->playlist, TRUE, FALSE); // Set our new current playlist and start playing immediately
}
void koto_album_set_description(
KotoAlbum * self,
const gchar * description
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!koto_utils_string_is_valid(description)) {
return;
}
self->description = g_strdup(description);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]);
}
void koto_album_set_narrator(
KotoAlbum * self,
const gchar * narrator
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!koto_utils_string_is_valid(narrator)) {
return;
}
self->narrator = g_strdup(narrator);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]);
}
void koto_album_set_path(
KotoAlbum * self,
KotoLibrary * lib,
const gchar * fixed_path
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path
gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library
gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path
g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one
koto_album_set_album_name(self, g_path_get_basename(relative_path)); // Update our album name based on the base name
if (!self->do_initial_index) { // Not doing our initial index
return;
}
koto_album_find_album_art(self); // Update our path for the album art
self->do_initial_index = FALSE;
}
void koto_album_set_preparsed_genres(
KotoAlbum * self,
gchar * genrelist
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string
return;
}
GList * preparsed_genres_list = koto_utils_string_to_string_list(genrelist, ";");
if (g_list_length(preparsed_genres_list) == 0) { // No genres
g_list_free(preparsed_genres_list);
return;
}
// TODO: Do a pass on in first memory optimization phase to ensure string elements are freed.
g_list_free_full(self->genres, NULL); // Free the existing genres list
self->genres = preparsed_genres_list;
};
void koto_album_set_uuid(
KotoAlbum * self,
const gchar * uuid
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (!koto_utils_string_is_valid(uuid)) {
return;
}
self->uuid = g_strdup(uuid);
g_object_set(
self->playlist,
"album-uuid",
self->uuid,
"uuid",
self->uuid, // Ensure the playlist has the same UUID as the album
NULL
);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]);
}
void koto_album_set_year(
KotoAlbum * self,
guint64 year
) {
if (!KOTO_IS_ALBUM(self)) { // Not an album
return;
}
if (year <= 0) {
return;
}
self->year = year;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]);
}
KotoAlbum * koto_album_new(gchar * artist_uuid) {
if (!koto_utils_string_is_valid(artist_uuid)) { // Invalid artist UUID provided
return NULL;
}
KotoAlbum * album = g_object_new(
KOTO_TYPE_ALBUM,
"artist-uuid",
artist_uuid,
"uuid",
g_strdup(g_uuid_string_random()),
"do-initial-index",
TRUE,
NULL
);
return album;
}
KotoAlbum * koto_album_new_with_uuid(
KotoArtist * artist,
const gchar * uuid
) {
gchar * artist_uuid = koto_artist_get_uuid(artist);
return g_object_new(
KOTO_TYPE_ALBUM,
"artist-uuid",
artist_uuid,
"uuid",
g_strdup(uuid),
"do-initial-index",
FALSE,
NULL
);
}

View file

@ -1,25 +0,0 @@
/* artist-playlist-funcs.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "../playlist/playlist.h"
#include "structs.h"
G_BEGIN_DECLS
KotoPlaylist * koto_artist_get_playlist(KotoArtist * self);
G_END_DECLS

View file

@ -1,606 +0,0 @@
/* artist.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <sqlite3.h>
#include "../config/config.h"
#include "../db/db.h"
#include "../db/cartographer.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "artist-playlist-funcs.h"
#include "structs.h"
#include "track-helpers.h"
extern KotoCartographer * koto_maps;
extern KotoConfig * config;
enum {
PROP_0,
PROP_UUID,
PROP_ARTIST_NAME,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
enum {
SIGNAL_ALBUM_ADDED,
SIGNAL_ALBUM_REMOVED,
SIGNAL_HAS_NO_ALBUMS,
SIGNAL_TRACK_ADDED,
SIGNAL_TRACK_REMOVED,
N_SIGNALS
};
static guint artist_signals[N_SIGNALS] = {
0
};
struct _KotoArtist {
GObject parent_instance;
gchar * uuid;
KotoPlaylist * content_playlist;
gboolean finalized;
gboolean has_artist_art;
gchar * artist_name;
GList * tracks;
GHashTable * paths;
KotoLibraryType type;
GQueue * albums;
GListStore * albums_store;
};
struct _KotoArtistClass {
GObjectClass parent_class;
void (* album_added) (
KotoArtist * artist,
KotoAlbum * album
);
void (* album_removed) (
KotoArtist * artist,
KotoAlbum * album
);
void (* has_no_albums) (KotoArtist * artist);
void (* track_added) (
KotoArtist * artist,
KotoTrack * track
);
void (* track_removed) (
KotoArtist * artist,
KotoTrack * track
);
};
G_DEFINE_TYPE(KotoArtist, koto_artist, G_TYPE_OBJECT);
static void koto_artist_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_artist_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_artist_class_init(KotoArtistClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_artist_set_property;
gobject_class->get_property = koto_artist_get_property;
artist_signals[SIGNAL_ALBUM_ADDED] = g_signal_new(
"album-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoArtistClass, album_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_ALBUM
);
artist_signals[SIGNAL_ALBUM_REMOVED] = g_signal_new(
"album-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoArtistClass, album_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_ALBUM
);
artist_signals[SIGNAL_HAS_NO_ALBUMS] = g_signal_new(
"has-no-albums",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoArtistClass, has_no_albums),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
artist_signals[SIGNAL_TRACK_ADDED] = g_signal_new(
"track-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoArtistClass, track_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_TRACK
);
artist_signals[SIGNAL_TRACK_REMOVED] = g_signal_new(
"track-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoArtistClass, track_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_TRACK
);
props[PROP_UUID] = g_param_spec_string(
"uuid",
"UUID to Artist in database",
"UUID to Artist in database",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_ARTIST_NAME] = g_param_spec_string(
"name",
"Name",
"Name of Artist",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_artist_init(KotoArtist * self) {
self->albums = g_queue_new(); // Create a new GQueue
self->albums_store = g_list_store_new(KOTO_TYPE_ALBUM); // Create our GListStore of type KotoAlbum
self->content_playlist = koto_playlist_new(); // Create our playlist
g_object_set(
self->content_playlist,
"ephemeral", // Indicate that it is temporary
TRUE,
NULL
);
self->finalized = FALSE; // Indicate we not finalized
self->has_artist_art = FALSE;
self->paths = g_hash_table_new(g_str_hash, g_str_equal);
self->tracks = NULL;
self->type = KOTO_LIBRARY_TYPE_UNKNOWN;
}
void koto_artist_commit(KotoArtist * self) {
if ((self->uuid == NULL) || strcmp(self->uuid, "")) { // UUID not set
self->uuid = g_strdup(g_uuid_string_random());
}
// TODO: Support multiple types instead of just local music artist
gchar * commit_op = g_strdup_printf(
"INSERT INTO artists(id , name, art_path)"
"VALUES ('%s', quote(\"%s\"), NULL)"
"ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;",
self->uuid,
koto_utils_string_get_valid(self->artist_name)
);
new_transaction(commit_op, "Failed to write our artist to the database", FALSE);
GHashTableIter paths_iter;
g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths
gpointer lib_uuid_ptr, artist_rel_path_ptr;
while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &artist_rel_path_ptr)) {
gchar * lib_uuid = lib_uuid_ptr;
gchar * artist_rel_path = artist_rel_path_ptr;
gchar * commit_op = g_strdup_printf(
"INSERT INTO libraries_artists(id, artist_id, path)"
"VALUES ('%s', '%s', quote(\"%s\"))"
"ON CONFLICT(id, artist_id) DO UPDATE SET path=excluded.path;",
lib_uuid,
self->uuid,
artist_rel_path
);
new_transaction(commit_op, "Failed to add this path for the artist", FALSE);
}
}
static void koto_artist_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoArtist * self = KOTO_ARTIST(obj);
switch (prop_id) {
case PROP_UUID:
g_value_set_string(val, self->uuid);
break;
case PROP_ARTIST_NAME:
g_value_set_string(val, self->artist_name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_artist_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoArtist * self = KOTO_ARTIST(obj);
switch (prop_id) {
case PROP_UUID:
self->uuid = g_strdup(g_value_get_string(val));
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]);
break;
case PROP_ARTIST_NAME:
koto_artist_set_artist_name(self, (gchar*) g_value_get_string(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_artist_add_album(
KotoArtist * self,
KotoAlbum * album
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
if (!KOTO_IS_ALBUM(album)) { // Album provided is not an album
return;
}
GList * found_albums = g_queue_find(self->albums, album); // Try finding the album
if (found_albums != NULL) { // Already has been added
g_list_free(found_albums);
return;
}
g_list_free(found_albums);
g_queue_push_tail(self->albums, album); // Add the album to end of albums GQueue
g_list_store_append(self->albums_store, album); // Add the album to th estore as well
if (self->finalized) { // Is already finalized
koto_artist_apply_model(self, koto_config_get_preferred_album_sort_type(config)); // Apply our model to sort our albums gqueue and store
}
g_signal_emit(
self,
artist_signals[SIGNAL_ALBUM_ADDED],
0,
album
);
}
void koto_artist_add_track(
KotoArtist * self,
KotoTrack * track
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
gchar * track_uuid = koto_track_get_uuid(track);
if (g_list_index(self->tracks, track_uuid) != -1) { // If we have already added the track
return;
}
koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary
self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_track_helpers_sort_tracks_by_uuid, NULL);
koto_playlist_add_track(self->content_playlist, track, FALSE, FALSE); // Add this new track for the artist to its playlist
g_signal_emit(
self,
artist_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
void koto_artist_apply_model(
KotoArtist * self,
KotoPreferredAlbumSortType model
) {
if (!KOTO_IS_ARTIST(self)) {
return;
}
g_queue_sort(self->albums, koto_artist_model_sort_albums, &model);
g_list_store_sort(self->albums_store, koto_artist_model_sort_albums, &model);
}
GQueue * koto_artist_get_albums(KotoArtist * self) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return NULL;
}
return self->albums;
}
GListStore * koto_artist_get_albums_store(KotoArtist * self) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return NULL;
}
return self->albums_store;
}
KotoAlbum * koto_artist_get_album_by_name(
KotoArtist * self,
gchar * album_name
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return NULL;
}
KotoAlbum * album = NULL;
GList * cur_list_iter;
for (cur_list_iter = self->albums->head; cur_list_iter != NULL; cur_list_iter = cur_list_iter->next) { // Iterate through our albums by their UUIDs
KotoAlbum * this_album = cur_list_iter->data;
if (!KOTO_IS_ALBUM(this_album)) { // Not an album
continue;
}
if (g_strcmp0(koto_album_get_name(this_album), album_name) == 0) { // These album names match
album = this_album;
break;
}
}
return album;
}
gchar * koto_artist_get_name(KotoArtist * self) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return g_strdup("");
}
return g_strdup(koto_utils_string_is_valid(self->artist_name) ? self->artist_name : ""); // Return artist name if set
}
KotoPlaylist * koto_artist_get_playlist(KotoArtist * self) {
if (!KOTO_IS_ARTIST(self)) {
return NULL;
}
return self->content_playlist;
}
GList * koto_artist_get_tracks(KotoArtist * self) {
return KOTO_IS_ARTIST(self) ? self->tracks : NULL;
}
KotoLibraryType koto_artist_get_lib_type(KotoArtist * self) {
return KOTO_IS_ARTIST(self) ? self->type : KOTO_LIBRARY_TYPE_UNKNOWN;
}
gchar * koto_artist_get_uuid(KotoArtist * self) {
return KOTO_IS_ARTIST(self) ? self->uuid : NULL;
}
void koto_artist_remove_album(
KotoArtist * self,
KotoAlbum * album
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
if (!KOTO_ALBUM(album)) { // No album defined
return;
}
g_queue_remove(self->albums, album); // Remove the album
guint position = 0;
if (g_list_store_find(self->albums_store, album, &position)) { // Found the album in the list store
g_list_store_remove(self->albums_store, position); // Remove from the list store
}
g_signal_emit(
self,
artist_signals[SIGNAL_ALBUM_REMOVED],
0,
album
);
}
void koto_artist_remove_track(
KotoArtist * self,
KotoTrack * track
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
if (!KOTO_IS_TRACK(track)) { // Not a track
return;
}
gchar * track_uuid = koto_track_get_uuid(track);
self->tracks = g_list_remove(self->tracks, koto_track_get_uuid(track));
koto_playlist_remove_track_by_uuid(self->content_playlist, track_uuid); // Remove the track from our playlist
g_signal_emit(
self,
artist_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
void koto_artist_set_artist_name(
KotoArtist * self,
gchar * artist_name
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
if (!koto_utils_string_is_valid(artist_name)) { // No artist name
return;
}
if (koto_utils_string_is_valid(self->artist_name)) { // Has artist name
g_free(self->artist_name);
}
self->artist_name = g_strdup(artist_name);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_NAME]);
}
void koto_artist_set_as_finalized(KotoArtist * self) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
self->finalized = TRUE;
if (g_queue_get_length(self->albums) == 0) { // Have no albums
g_signal_emit_by_name(self, "has-no-albums");
}
}
void koto_artist_set_path(
KotoArtist * self,
KotoLibrary * lib,
const gchar * fixed_path,
gboolean should_commit
) {
if (!KOTO_IS_ARTIST(self)) { // Not an artist
return;
}
gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path
gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library
gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path
g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one
if (should_commit) { // Should commit to the DB
koto_artist_commit(self); // Save the artist
}
self->type = koto_library_get_lib_type(lib); // Define our artist type as the type from the library
}
gint koto_artist_model_sort_albums(
gconstpointer first_item,
gconstpointer second_item,
gpointer user_data
) {
KotoPreferredAlbumSortType * preferred_model = (KotoPreferredAlbumSortType*) user_data;
KotoAlbum * first_album = KOTO_ALBUM(first_item);
KotoAlbum * second_album = KOTO_ALBUM(second_item);
if (KOTO_IS_ALBUM(first_album) && !KOTO_IS_ALBUM(second_album)) { // First album is valid, second isn't
return -1;
} else if (!KOTO_IS_ALBUM(first_album) && KOTO_IS_ALBUM(second_album)) { // Second album is valid, first isn't
return 1;
}
if (preferred_model == KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT) { // Sort chronological before alphabetical
guint64 fa_year = koto_album_get_year(first_album);
guint64 sa_year = koto_album_get_year(second_album);
if (fa_year > sa_year) { // First album is newer than second
return -1;
} else if (fa_year < sa_year) { // Second album is newer than first
return 1;
}
}
return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album));
}
KotoArtist * koto_artist_new(gchar * artist_name) {
KotoArtist * artist = g_object_new(
KOTO_TYPE_ARTIST,
"uuid",
g_uuid_string_random(),
"name",
artist_name,
NULL
);
return artist;
}
KotoArtist * koto_artist_new_with_uuid(const gchar * uuid) {
return g_object_new(
KOTO_TYPE_ARTIST,
"uuid",
g_strdup(uuid),
NULL
);
}

View file

@ -1,239 +0,0 @@
/* file-indexer.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <dirent.h>
#include <magic.h>
#include <stdio.h>
#include <sys/stat.h>
#include "../db/cartographer.h"
#include "../koto-utils.h"
#include "structs.h"
#include "track-helpers.h"
extern KotoCartographer * koto_maps;
extern magic_t magic_cookie;
void index_folder(
KotoLibrary * self,
gchar * path,
guint depth
) {
depth++;
DIR * dir = opendir(path); // Attempt to open our directory
if (dir == NULL) {
return;
}
struct dirent * entry;
while ((entry = readdir(dir))) {
if (g_str_has_prefix(entry->d_name, ".")) { // A reference to parent dir, self, or a hidden item
continue;
}
gchar * full_path = g_strdup_printf("%s%s%s", path, G_DIR_SEPARATOR_S, entry->d_name);
if (entry->d_type == DT_DIR) { // Directory
if (depth == 1) { // If we are following (ARTIST,AUTHOR,PODCAST)/ALBUM then this would be artist
KotoArtist * artist = koto_artist_new(entry->d_name); // Attempt to get the artist
if (KOTO_IS_ARTIST(artist)) {
koto_artist_set_path(artist, self, full_path, TRUE); // Add the path for this library on this Artist and commit immediately
koto_cartographer_add_artist(koto_maps, artist); // Add the artist to cartographer
index_folder(self, full_path, depth); // Index this directory
koto_artist_set_as_finalized(artist); // Indicate it is finalized
}
} else if (depth == 2) { // If we are following FOLDER/ARTIST/ALBUM then this would be album
gchar * artist_name = g_path_get_basename(path); // Get the last entry from our path which is probably the artist
KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name);
if (!KOTO_IS_ARTIST(artist)) { // Not an artist
continue;
}
gchar * artist_uuid = koto_artist_get_uuid(artist); // Get the artist's UUID
KotoAlbum * album = koto_album_new(artist_uuid);
koto_album_set_path(album, self, full_path);
koto_cartographer_add_album(koto_maps, album); // Add our album to the cartographer
koto_artist_add_album(artist, album); // Add the album
index_folder(self, full_path, depth); // Index inside the album
koto_album_commit(album); // Save to database immediately
g_free(artist_name);
} else if (depth == 3) { // Possibly CD within album
gchar ** split = g_strsplit(full_path, G_DIR_SEPARATOR_S, -1);
guint split_len = g_strv_length(split);
if (split_len < 4) {
g_strfreev(split);
continue;
}
gchar * album_name = g_strdup(split[split_len - 2]);
gchar * artist_name = g_strdup(split[split_len - 3]);
g_strfreev(split);
if (!koto_utils_string_is_valid(album_name)) {
g_free(album_name);
continue;
}
if (!koto_utils_string_is_valid(artist_name)) {
g_free(album_name);
g_free(artist_name);
continue;
}
KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_name);
g_free(artist_name);
if (!KOTO_IS_ARTIST(artist)) {
continue;
}
KotoAlbum * album = koto_artist_get_album_by_name(artist, album_name); // Get the album
g_free(album_name);
if (!KOTO_IS_ALBUM(album)) {
continue;
}
index_folder(self, full_path, depth); // Index inside the album
}
} else if ((entry->d_type == DT_REG)) { // Is a file in artist folder or lower in FS hierarchy
index_file(self, full_path); // Index this audio file or weird ogg thing
}
g_free(full_path);
}
closedir(dir); // Close the directory
}
void index_file(
KotoLibrary * lib,
const gchar * path
) {
const char * mime_type = magic_file(magic_cookie, path);
if (mime_type == NULL) { // Failed to get the mimetype
return;
}
if (!g_str_has_prefix(mime_type, "audio/") && !g_str_has_prefix(mime_type, "video/ogg")) { // Is not an audio file or ogg
return;
}
gboolean for_audiobook = (koto_library_get_lib_type(lib) == KOTO_LIBRARY_TYPE_AUDIOBOOK);
gchar * relative_path_to_file = koto_library_get_relative_path_to_file(lib, g_strdup(path)); // Strip out library path so we have a relative path to the file
gchar * file_basename = g_path_get_basename(relative_path_to_file);
gchar ** split_on_relative_slashes = g_strsplit(relative_path_to_file, G_DIR_SEPARATOR_S, -1); // Split based on separator (e.g. / )
guint slash_sep_count = g_strv_length(split_on_relative_slashes);
gchar * artist_author_podcast_name = g_strdup(split_on_relative_slashes[0]); // No matter what, artist should be first
gchar * album_or_audiobook_name = NULL;
guint cd = (guint) 0;
if (slash_sep_count >= 3) { // If this it is at least "artist" + "album" + "file" (or with CD)
album_or_audiobook_name = g_strdup(split_on_relative_slashes[1]); // Duplicate the second item as the album or audiobook name
}
// #region CD parsing logic
if ((slash_sep_count == 4)) { // If is at least "artist" + "album" + "cd" + "file"
gchar * cd_str = g_strdup(g_strstrip(koto_utils_string_replace_all(g_utf8_strdown(split_on_relative_slashes[2], -1), "cd", ""))); // Replace a lowercased version of our CD ("cd") and trim any whitespace
cd = (guint) g_ascii_strtoull(cd_str, NULL, 10); // Attempt to convert}
}
if (for_audiobook && (cd == 0)) { // No CD yet and is for an audiobook
cd = koto_track_helpers_get_cd_based_on_file_name(file_basename); // Base on file name
} else {
cd = 1;
}
// #endregion
gchar * file_name = NULL;
if (for_audiobook) { // If this is for an audiobook library
guint64 pos = koto_track_helpers_get_position_based_on_file_name(file_basename);
if (cd != 1) { // On a non "Part 1" audiobook
file_name = g_strdup_printf("%s - Part %u - %lu", album_or_audiobook_name, cd, pos);
} else { // Part 1 / CD 1
file_name = g_strdup_printf("%s - %lu", album_or_audiobook_name, pos);
}
}
if (!koto_utils_string_is_valid(file_name)) { // No valid file name yet
file_name = koto_track_helpers_get_name_for_file(path, artist_author_podcast_name); // Get the name of the file
}
g_strfreev(split_on_relative_slashes);
g_free(file_basename);
gchar * sorta_uniqueish_key = NULL;
if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have audiobook or album name
sorta_uniqueish_key = g_strdup_printf("%s-%s-%s", artist_author_podcast_name, album_or_audiobook_name, file_name);
} else { // No audiobook or album name
sorta_uniqueish_key = g_strdup_printf("%s-%s", artist_author_podcast_name, file_name);
}
KotoTrack * track = koto_cartographer_get_track_by_uniqueish_key(koto_maps, sorta_uniqueish_key); // Attempt to get any existing KotoTrack
if (KOTO_IS_TRACK(track)) { // Got a track already
koto_track_set_path(track, lib, relative_path_to_file); // Add this path, which will determine the associated library within that function
} else { // Don't already have a track for this file
KotoArtist * artist = koto_cartographer_get_artist_by_name(koto_maps, artist_author_podcast_name); // Get the possible artist
if (!KOTO_IS_ARTIST(artist)) { // Have an artist for this already
return;
}
KotoAlbum * album = NULL;
if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have an album or audiobook name
KotoAlbum * possible_album = koto_artist_get_album_by_name(artist, album_or_audiobook_name);
album = KOTO_IS_ALBUM(possible_album) ? possible_album : NULL;
}
gchar * album_uuid = KOTO_IS_ALBUM(album) ? koto_album_get_uuid(album) : NULL;
track = koto_track_new(koto_artist_get_uuid(artist), album_uuid, file_name, cd);
koto_track_set_path(track, lib, relative_path_to_file); // Immediately add the path to this file, for this Library
koto_artist_add_track(artist, track); // Add the track to the artist in the event this is a podcast (no album) or the track is directly in the artist directory
if (KOTO_IS_ALBUM(album)) { // Have an album
koto_album_add_track(album, track); // Add this track since we haven't yet
}
koto_cartographer_add_track(koto_maps, track); // Add to our cartographer tracks hashtable
}
if (KOTO_IS_TRACK(track)) { // Is a track
koto_track_commit(track); // Save the track immediately
}
}

View file

@ -1,511 +0,0 @@
/* library.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "../config/config.h"
#include "../koto-utils.h"
#include "structs.h"
extern KotoConfig * config;
extern GVolumeMonitor * volume_monitor;
enum {
PROP_0,
PROP_TYPE,
PROP_UUID,
PROP_STORAGE_UUID,
PROP_CONSTRUCTION_PATH,
PROP_NAME,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL,
};
enum {
SIGNAL_NOW_AVAILABLE,
SIGNAL_NOW_UNAVAILABLE,
N_SIGNALS
};
static guint library_signals[N_SIGNALS] = {
0
};
struct _KotoLibrary {
GObject parent_instance;
gchar * uuid;
KotoLibraryType type;
gchar * directory;
gchar * storage_uuid;
GMount * mount;
gulong mount_unmounted_handler;
gchar * mount_path;
gboolean should_index;
gchar * path;
gchar * relative_path;
gchar * name;
};
struct _KotoLibraryClass {
GObjectClass parent_class;
void (* now_available) (KotoLibrary * library);
void (* now_unavailable) (KotoLibrary * library);
};
G_DEFINE_TYPE(KotoLibrary, koto_library, G_TYPE_OBJECT);
static void koto_library_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_library_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_library_class_init(KotoLibraryClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_library_set_property;
gobject_class->get_property = koto_library_get_property;
library_signals[SIGNAL_NOW_AVAILABLE] = g_signal_new(
"now-available",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoLibraryClass, now_available),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
library_signals[SIGNAL_NOW_UNAVAILABLE] = g_signal_new(
"now-unavailable",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoLibraryClass, now_unavailable),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
props[PROP_UUID] = g_param_spec_string(
"uuid",
"UUID of Library",
"UUID of Library",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_TYPE] = g_param_spec_string(
"type",
"Type of Library",
"Type of Library",
koto_library_type_to_string(KOTO_LIBRARY_TYPE_MUSIC),
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_STORAGE_UUID] = g_param_spec_string(
"storage-uuid",
"Storage UUID to associated Mount of Library",
"Storage UUID to associated Mount of Library",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_CONSTRUCTION_PATH] = g_param_spec_string(
"construction-path",
"Construction Path",
"Path to this library during construction",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
props[PROP_NAME] = g_param_spec_string(
"name",
"Name of the Library",
"Name of the Library",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_library_init(KotoLibrary * self) {
(void) self;
}
static void koto_library_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoLibrary * self = KOTO_LIBRARY(obj);
switch (prop_id) {
case PROP_NAME:
g_value_set_string(val, g_strdup(self->name));
break;
case PROP_UUID:
g_value_set_string(val, self->uuid);
break;
case PROP_STORAGE_UUID:
g_value_set_string(val, self->storage_uuid);
break;
case PROP_TYPE:
g_value_set_string(val, koto_library_type_to_string(self->type));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_library_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoLibrary * self = KOTO_LIBRARY(obj);
switch (prop_id) {
case PROP_UUID:
self->uuid = g_strdup(g_value_get_string(val));
break;
case PROP_TYPE:
self->type = koto_library_type_from_string(g_strdup(g_value_get_string(val)));
break;
case PROP_STORAGE_UUID:
koto_library_set_storage_uuid(self, g_strdup(g_value_get_string(val)));
break;
case PROP_CONSTRUCTION_PATH:
koto_library_set_path(self, g_strdup(g_value_get_string(val)));
break;
case PROP_NAME:
koto_library_set_name(self, g_strdup(g_value_get_string(val)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
gchar * koto_library_get_path(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self)) {
return NULL;
}
if (G_IS_MOUNT(self->mount)) {
}
return self->path;
}
gchar * koto_library_get_relative_path_to_file(
KotoLibrary * self,
gchar * full_path
) {
if (!KOTO_IS_LIBRARY(self)) {
return NULL;
}
gchar * appended_slash_to_library_path = g_str_has_suffix(self->path, G_DIR_SEPARATOR_S) ? g_strdup(self->path) : g_strdup_printf("%s%s", g_strdup(self->path), G_DIR_SEPARATOR_S);
gchar * cleaned_path = koto_utils_string_replace_all(full_path, appended_slash_to_library_path, ""); // Replace the full path
g_free(appended_slash_to_library_path);
return cleaned_path;
}
gchar * koto_library_get_storage_uuid(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self)) {
return NULL;
}
return self->storage_uuid;
}
KotoLibraryType koto_library_get_lib_type(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self)) {
return KOTO_LIBRARY_TYPE_UNKNOWN;
}
return self->type;
}
gchar * koto_library_get_uuid(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self)) {
return NULL;
}
return self->uuid;
}
void koto_library_index(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self) || !self->should_index) { // Not a library or should not index
return;
}
index_folder(self, self->path, 0); // Start index operation at the top
}
gboolean koto_library_is_available(KotoLibrary * self) {
if (!KOTO_IS_LIBRARY(self)) {
return FALSE;
}
return FALSE;
}
void koto_library_set_name(
KotoLibrary * self,
gchar * library_name
) {
if (!KOTO_IS_LIBRARY(self)) {
return;
}
if (!koto_utils_string_is_valid(library_name)) { // Not a string
return;
}
if (koto_utils_string_is_valid(self->name)) { // Name already set
g_free(self->name); // Free the existing value
}
self->name = g_strdup(library_name);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]);
}
void koto_library_set_path(
KotoLibrary * self,
gchar * path
) {
if (!KOTO_IS_LIBRARY(self)) {
return;
}
if (!koto_utils_string_is_valid(path)) { // Not a valid string
return;
}
if (koto_utils_string_is_valid(self->path)) {
g_free(self->path);
}
self->relative_path = g_path_is_absolute(path) ? koto_utils_string_replace_all(path, self->mount_path, "") : path; // Ensure path is relative to our mount, even if the mount is really our own system partition
self->path = g_build_path(G_DIR_SEPARATOR_S, self->mount_path, self->relative_path, NULL); // Ensure our path is to whatever the current path of the mount + relative path is
}
void koto_library_set_storage_uuid(
KotoLibrary * self,
gchar * storage_uuid
) {
if (!KOTO_IS_LIBRARY(self)) {
return;
}
if (G_IS_MOUNT(self->mount)) { // Already have a mount
g_signal_handler_disconnect(self->mount, self->mount_unmounted_handler); // Stop listening to the unmounted signal for this existing mount
g_object_unref(self->mount); // Dereference the mount
g_free(self->mount_path);
}
if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid string, which actually is allowed for built-ins
self->mount = NULL;
self->mount_path = g_strdup_printf("%s%s", g_get_home_dir(), G_DIR_SEPARATOR_S); // Set mount path to user's home directory
self->storage_uuid = NULL;
return;
}
GMount * mount = g_volume_monitor_get_mount_for_uuid(volume_monitor, storage_uuid); // Attempt to get the mount by this UUID
if (!G_IS_MOUNT(mount)) {
g_warning("Failed to get mount for UUID: %s", storage_uuid);
self->mount = NULL;
return;
}
if (g_mount_is_shadowed(mount)) { // Is shadowed and should not use
g_warning("This mount is considered \"shadowed\" and will not be used.");
return;
}
GFile * mount_file = g_mount_get_default_location(mount); // Get the file for the entry location of the mount
self->mount = mount;
self->mount_path = g_strdup(g_file_get_path(mount_file)); // Set the mount path to the path defined for the Mount File
self->storage_uuid = g_strdup(storage_uuid);
}
gchar * koto_library_to_config_string(KotoLibrary * self) {
GStrvBuilder * lib_builder = g_strv_builder_new(); // Create new strv builder
g_strv_builder_add(lib_builder, g_strdup("[[library]]")); // Add our library array header
g_strv_builder_add(lib_builder, g_strdup_printf("\tdirectory=\"%s\"", self->relative_path)); // Add the directory
if (koto_utils_string_is_valid(self->name)) { // Have a library name
g_strv_builder_add(lib_builder, g_strdup_printf("\tname=\"%s\"", self->name)); // Add the name
}
if (koto_utils_string_is_valid(self->storage_uuid)) { // Have a storage UUID (not applicable to built-ins)
g_strv_builder_add(lib_builder, g_strdup_printf("\tstorage_uuid=\"%s\"", self->storage_uuid)); // Add the storage UUID
}
g_strv_builder_add(lib_builder, g_strdup_printf("\ttype=\"%s\"", koto_library_type_to_string(self->type))); // Add the type
g_strv_builder_add(lib_builder, g_strdup_printf("\tuuid=\"%s\"", self->uuid));
GStrv lines = g_strv_builder_end(lib_builder); // Get all the lines as a GStrv which is a gchar **
gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline
g_strfreev(lines); // Free our lines
g_strv_builder_unref(lib_builder); // Unref our builder
return g_strdup(content);
}
KotoLibrary * koto_library_new(
KotoLibraryType type,
const gchar * storage_uuid,
const gchar * path
) {
KotoLibrary * lib = g_object_new(
KOTO_TYPE_LIBRARY,
"type",
koto_library_type_to_string(type),
"uuid",
g_uuid_string_random(), // Create a new Library with a new UUID
"storage-uuid",
storage_uuid,
"construction-path",
path,
NULL
);
lib->should_index = TRUE;
return lib;
}
KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum) {
toml_datum_t uuid_datum = toml_string_in(lib_datum, "uuid"); // Get the library UUID
if (!uuid_datum.ok) { // No UUID defined
g_warning("No UUID set for this library. Ignoring");
return NULL;
}
gchar * uuid = g_strdup(uuid_datum.u.s); // Duplicate our UUID
toml_datum_t type_datum = toml_string_in(lib_datum, "type");
if (!type_datum.ok) { // No type defined
g_warning("Unknown type for library with UUID of %s", uuid);
return NULL;
}
gchar * lib_type_as_str = g_strdup(type_datum.u.s);
KotoLibraryType lib_type = koto_library_type_from_string(lib_type_as_str); // Get the library's type
if (lib_type == KOTO_LIBRARY_TYPE_UNKNOWN) { // Known type
return NULL;
}
toml_datum_t dir_datum = toml_string_in(lib_datum, "directory");
if (!dir_datum.ok) {
g_critical("Failed to get directory path for library with UUID of %s", uuid);
return NULL;
}
gchar * path = g_strdup(dir_datum.u.s); // Duplicate the path string
toml_datum_t storage_uuid_datum = toml_string_in(lib_datum, "storage_uuid"); // Get the datum for the storage UUID
gchar * storage_uuid = g_strdup((storage_uuid_datum.ok) ? storage_uuid_datum.u.s : "");
toml_datum_t name_datum = toml_string_in(lib_datum, "name"); // Get the datum for the name
gchar * name = g_strdup((name_datum.ok) ? name_datum.u.s : "");
KotoLibrary * lib = g_object_new(
KOTO_TYPE_LIBRARY,
"type",
lib_type_as_str,
"uuid",
uuid,
"storage-uuid",
storage_uuid,
"construction-path",
path,
"name",
name,
NULL
);
lib->should_index = FALSE;
return lib;
}
KotoLibraryType koto_library_type_from_string(gchar * t) {
if (
(g_strcmp0(t, "audiobooks") == 0) ||
(g_strcmp0(t, "audiobook") == 0)
) {
return KOTO_LIBRARY_TYPE_AUDIOBOOK;
} else if (g_strcmp0(t, "music") == 0) {
return KOTO_LIBRARY_TYPE_MUSIC;
} else if (
(g_strcmp0(t, "podcasts") == 0) ||
(g_strcmp0(t, "podcast") == 0)
) {
return KOTO_LIBRARY_TYPE_PODCAST;
}
g_warning("Invalid type provided for koto_library_type_from_string: %s", t);
return KOTO_LIBRARY_TYPE_UNKNOWN;
}
gchar * koto_library_type_to_string(KotoLibraryType t) {
switch (t) {
case KOTO_LIBRARY_TYPE_AUDIOBOOK:
return g_strdup("audiobook");
case KOTO_LIBRARY_TYPE_MUSIC:
return g_strdup("music");
case KOTO_LIBRARY_TYPE_PODCAST:
return g_strdup("podcast");
default:
return g_strdup("UNKNOWN");
}
}

View file

@ -1,22 +0,0 @@
/* misc-types.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
typedef enum {
KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT, // Chronological is considered default
KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL, // Prefer sorting alphabetically
} KotoPreferredAlbumSortType;

View file

@ -1,425 +0,0 @@
/* structs.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib.h>
#include <glib-2.0/gio/gio.h>
#include <magic.h>
#include <toml.h>
#include "misc-types.h"
typedef enum {
KOTO_LIBRARY_TYPE_AUDIOBOOK = 1,
KOTO_LIBRARY_TYPE_MUSIC = 2,
KOTO_LIBRARY_TYPE_PODCAST = 3,
KOTO_LIBRARY_TYPE_UNKNOWN = 4
} KotoLibraryType;
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_LIBRARY koto_library_get_type()
#define KOTO_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_LIBRARY, KotoLibrary))
typedef struct _KotoLibrary KotoLibrary;
typedef struct _KotoLibraryClass KotoLibraryClass;
GLIB_AVAILABLE_IN_ALL
GType koto_library_get_type(void) G_GNUC_CONST;
#define KOTO_IS_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_LIBRARY))
#define KOTO_TYPE_ARTIST koto_artist_get_type()
#define KOTO_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ARTIST, KotoArtist))
typedef struct _KotoArtist KotoArtist;
typedef struct _KotoArtistClass KotoArtistClass;
GLIB_AVAILABLE_IN_ALL
GType koto_artist_get_type(void) G_GNUC_CONST;
#define KOTO_IS_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ARTIST))
#define KOTO_TYPE_ALBUM koto_album_get_type()
#define KOTO_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_ALBUM, KotoAlbum))
typedef struct _KotoAlbum KotoAlbum;
typedef struct _KotoAlbumClass KotoAlbumClass;
GLIB_AVAILABLE_IN_ALL
GType koto_album_get_type(void) G_GNUC_CONST;
#define KOTO_IS_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM))
#define KOTO_TYPE_TRACK koto_track_get_type()
G_DECLARE_FINAL_TYPE(KotoTrack, koto_track, KOTO, TRACK, GObject);
#define KOTO_IS_TRACK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_TRACK))
/**
* Library Functions
**/
KotoLibrary * koto_library_new(
KotoLibraryType type,
const gchar * storage_uuid,
const gchar * path
);
KotoLibrary * koto_library_new_from_toml_table(toml_table_t * lib_datum);
gchar * koto_library_get_path(KotoLibrary * self);
gchar * koto_library_get_relative_path_to_file(
KotoLibrary * self,
gchar * full_path
);
gchar * koto_library_get_storage_uuid(KotoLibrary * self);
KotoLibraryType koto_library_get_lib_type(KotoLibrary * self);
gchar * koto_library_get_uuid(KotoLibrary * self);
void koto_library_index(KotoLibrary * self);
gboolean koto_library_is_available(KotoLibrary * self);
gchar * koto_library_get_storage_uuid(KotoLibrary * self);
void koto_library_set_name(
KotoLibrary * self,
gchar * library_name
);
void koto_library_set_path(
KotoLibrary * self,
gchar * path
);
void koto_library_set_storage_uuid(
KotoLibrary * self,
gchar * uuid
);
gchar * koto_library_to_config_string(KotoLibrary * self);
KotoLibraryType koto_library_type_from_string(gchar * t);
gchar * koto_library_type_to_string(KotoLibraryType t);
void index_folder(
KotoLibrary * self,
gchar * path,
guint depth
);
void index_file(
KotoLibrary * lib,
const gchar * path
);
/**
* Artist Functions
**/
KotoArtist * koto_artist_new(gchar * artist_name);
KotoArtist * koto_artist_new_with_uuid(const gchar * uuid);
void koto_artist_add_album(
KotoArtist * self,
KotoAlbum * album
);
void koto_artist_add_track(
KotoArtist * self,
KotoTrack * track
);
void koto_artist_apply_model(
KotoArtist * self,
KotoPreferredAlbumSortType model
);
void koto_artist_commit(KotoArtist * self);
GQueue * koto_artist_get_albums(KotoArtist * self);
GListStore * koto_artist_get_albums_store(KotoArtist * self);
KotoAlbum * koto_artist_get_album_by_name(
KotoArtist * self,
gchar * album_name
);
gchar * koto_artist_get_name(KotoArtist * self);
gchar * koto_artist_get_path(KotoArtist * self);
GList * koto_artist_get_tracks(KotoArtist * self);
gchar * koto_artist_get_uuid(KotoArtist * self);
KotoLibraryType koto_artist_get_lib_type(KotoArtist * self);
gint koto_artist_model_sort_albums(
gconstpointer first_item,
gconstpointer second_item,
gpointer user_data
);
void koto_artist_remove_album(
KotoArtist * self,
KotoAlbum * album
);
void koto_artist_remove_track(
KotoArtist * self,
KotoTrack * track
);
void koto_artist_set_artist_name(
KotoArtist * self,
gchar * artist_name
);
void koto_artist_set_as_finalized(KotoArtist * self);
void koto_artist_set_path(
KotoArtist * self,
KotoLibrary * lib,
const gchar * fixed_path,
gboolean should_commit
);
/**
* Album Functions
**/
KotoAlbum * koto_album_new(gchar * artist_uuid);
KotoAlbum * koto_album_new_with_uuid(
KotoArtist * artist,
const gchar * uuid
);
void koto_album_add_track(
KotoAlbum * self,
KotoTrack * track
);
void koto_album_commit(KotoAlbum * self);
void koto_album_find_album_art(KotoAlbum * self);
gchar * koto_album_get_art(KotoAlbum * self);
gchar * koto_album_get_artist_uuid(KotoAlbum * self);
gchar * koto_album_get_description(KotoAlbum * self);
GList * koto_album_get_genres(KotoAlbum * self);
KotoLibraryType koto_album_get_lib_type(KotoAlbum * self);
gchar * koto_album_get_name(KotoAlbum * self);
gchar * koto_album_get_narrator(KotoAlbum * self);
gchar * koto_album_get_path(KotoAlbum * self);
GListStore * koto_album_get_store(KotoAlbum * self);
gchar * koto_album_get_uuid(KotoAlbum * self);
guint64 koto_album_get_year(KotoAlbum * self);
void koto_album_mark_as_finalized(KotoAlbum * self);
void koto_album_remove_track(
KotoAlbum * self,
KotoTrack * track
);
void koto_album_set_album_art(
KotoAlbum * self,
const gchar * album_art
);
void koto_album_set_album_name(
KotoAlbum * self,
const gchar * album_name
);
void koto_album_set_artist_uuid(
KotoAlbum * self,
const gchar * artist_uuid
);
void koto_album_set_description(
KotoAlbum * self,
const char * description
);
void koto_album_set_as_current_playlist(KotoAlbum * self);
void koto_album_set_narrator(
KotoAlbum * self,
const char * narrator
);
void koto_album_set_path(
KotoAlbum * self,
KotoLibrary * lib,
const gchar * fixed_path
);
void koto_album_set_preparsed_genres(
KotoAlbum * self,
gchar * genrelist
);
void koto_album_set_uuid(
KotoAlbum * self,
const gchar * uuid
);
void koto_album_set_year(
KotoAlbum * self,
guint64 year
);
/**
* File / Track Functions
**/
KotoTrack * koto_track_new(
const gchar * artist_uuid,
const gchar * album_uuid,
const gchar * parsed_name,
guint cd
);
KotoTrack * koto_track_new_with_uuid(const gchar * uuid);
void koto_track_commit(KotoTrack * self);
gchar * koto_track_get_description(KotoTrack * self);
guint koto_track_get_disc_number(KotoTrack * self);
guint64 koto_track_get_duration(KotoTrack * self);
GList * koto_track_get_genres(KotoTrack * self);
GVariant * koto_track_get_metadata_vardict(KotoTrack * self);
gchar * koto_track_get_path(KotoTrack * self);
gchar * koto_track_get_name(KotoTrack * self);
gchar * koto_track_get_narrator(KotoTrack * self);
guint64 koto_track_get_playback_position(KotoTrack * self);
guint64 koto_track_get_position(KotoTrack * self);
gchar * koto_track_get_uniqueish_key(KotoTrack * self);
gchar * koto_track_get_uuid(KotoTrack * self);
guint64 koto_track_get_year(KotoTrack * self);
void koto_track_remove_from_playlist(
KotoTrack * self,
gchar * playlist_uuid
);
void koto_track_set_album_uuid(
KotoTrack * self,
const gchar * album_uuid
);
void koto_track_save_to_playlist(
KotoTrack * self,
gchar * playlist_uuid
);
void koto_track_set_cd(
KotoTrack * self,
guint cd
);
void koto_track_set_description(
KotoTrack * self,
const gchar * description
);
void koto_track_set_duration(
KotoTrack * self,
guint64 duration
);
void koto_track_set_file_name(
KotoTrack * self,
gchar * new_file_name
);
void koto_track_set_genres(
KotoTrack * self,
char * genrelist
);
void koto_track_set_narrator(
KotoTrack * self,
const gchar * narrator
);
void koto_track_set_parsed_name(
KotoTrack * self,
gchar * new_parsed_name
);
void koto_track_set_path(
KotoTrack * self,
KotoLibrary * lib,
gchar * fixed_path
);
void koto_track_set_playback_position(
KotoTrack * self,
guint64 position
);
void koto_track_set_position(
KotoTrack * self,
guint64 pos
);
void koto_track_set_preparsed_genres(
KotoTrack * self,
gchar * genrelist
);
void koto_track_set_year(
KotoTrack * self,
guint64 year
);
void koto_track_update_metadata(KotoTrack * self);
G_END_DECLS

View file

@ -1,243 +0,0 @@
/* track-helpers.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <taglib/tag_c.h>
#include "../components/track-item.h"
#include "../db/cartographer.h"
#include "../koto-utils.h"
#include "structs.h"
extern KotoCartographer * koto_maps;
GHashTable * genre_replacements;
void koto_track_helpers_init() {
genre_replacements = g_hash_table_new(g_str_hash, g_str_equal);
gchar * corrected_genre_hiphop = g_strdup("hip-hop");
gchar * correct_genre_indie = g_strdup("indie");
gchar * correct_genre_scifi = g_strdup("sci-fi");
g_hash_table_insert(genre_replacements, g_strdup("alternative/indie"), correct_genre_indie); // Change Alternative/Indie (lowercased) to indie
g_hash_table_insert(genre_replacements, g_strdup("rap-&-hip-hop"), corrected_genre_hiphop); // Change rap-&-hip-hop to just hip-hop
g_hash_table_insert(genre_replacements, g_strdup("science-fiction"), correct_genre_scifi); // Change science-fiction to sci-fi
}
gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre) {
gchar * lookedup_genre = g_hash_table_lookup(genre_replacements, original_genre); // Look up the genre
return koto_utils_string_is_valid(lookedup_genre) ? lookedup_genre : original_genre;
}
guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name) {
gchar ** part_split = g_strsplit(file_name, "Part", -1);
gchar * part_str = NULL;
for (guint i = 0; i < g_strv_length(part_split); i++) { // Iterate on the parts
gchar * stripped_part = g_strdup(g_strstrip(part_split[i])); // Trim the whitespace around this part
gchar ** split = g_regex_split_simple("^([\\d]+)", stripped_part, G_REGEX_JAVASCRIPT_COMPAT, 0);
g_free(stripped_part); // Free the stripped part
if (g_strv_length(split) > 1) { // Has positional info at the beginning of the string
part_str = g_strdup(split[1]);
g_strfreev(split);
break;
} else {
g_strfreev(split);
}
}
guint64 cd = 0;
if (koto_utils_string_is_valid(part_str)) { // Have a valid string for the part
cd = g_ascii_strtoull(part_str, NULL, 10);
g_free(part_str);
}
if (cd == 0) {
cd = 1; // Should be first CD, not 0
}
return cd;
}
gchar * koto_track_helpers_get_name_for_file(
const gchar * path,
gchar * optional_artist_name
) {
gchar * file_name = NULL;
TagLib_File * t_file = taglib_file_new(path); // Get a taglib file for this file
if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid
TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag
file_name = g_strdup(taglib_tag_title(tag)); // Get the tag title and duplicate it
}
taglib_tag_free_strings(); // Free strings
taglib_file_free(t_file); // Free the file
if (koto_utils_string_is_valid(file_name)) { // File name not set yet
return file_name;
}
gchar * name_without_hyphen_surround = koto_utils_string_replace_all(koto_utils_get_filename_without_extension((gchar*) path), " - ", ""); // Remove - surrounded by spaces
gchar * name_without_hyphen = koto_utils_string_replace_all(name_without_hyphen_surround, "-", ""); // Remove just -
g_free(name_without_hyphen_surround);
file_name = koto_utils_string_replace_all(name_without_hyphen, "_", " "); // Replace underscore with whitespace
if (koto_utils_string_is_valid(optional_artist_name)) { // Was provided an optional artist name
gchar * replaced_artist = koto_utils_string_replace_all(file_name, optional_artist_name, ""); // Remove the artist
g_free(file_name);
file_name = g_strdup(replaced_artist);
g_free(replaced_artist);
}
gchar ** split = g_regex_split_simple("^([\\d]+)", file_name, G_REGEX_JAVASCRIPT_COMPAT, 0); // Split based on any possible position
if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file name
g_free(file_name); // Free the prior name
file_name = g_strdup(split[2]); // Set to our second item which is the rest of the song name without the prefixed numbers
}
g_strfreev(split);
return g_strdup(g_strstrip(file_name));
}
guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name) {
GRegex * num_pat = g_regex_new("^([\\d]+)", G_REGEX_JAVASCRIPT_COMPAT, 0, NULL);
gchar ** split = g_regex_split(num_pat, file_name, 0);
if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file
gchar * num = g_strdup(split[1]);
g_strfreev(split);
if ((strcmp(num, "0") != 0) && (strcmp(num, "00") != 0)) { // Is not zero
guint64 potential_pos = g_ascii_strtoull(num, NULL, 10); // Attempt to convert
if (potential_pos != 0) { // Got a legitimate position
g_free(num_pat); // Free our regex
return potential_pos; // Return this position
}
}
}
gchar * fn_no_ext = koto_utils_get_filename_without_extension((gchar*) file_name); // Get the filename without the extension
split = g_strsplit(fn_no_ext, ".", -1); // Split every time we see .
guint len_of_extension_split = g_strv_length(split);
gchar * fn_last_split_on_period = g_strdup(split[len_of_extension_split - 1]);
g_free(fn_no_ext);
g_strfreev(split); // Free our split
gchar ** whitespace_split = g_strsplit(fn_last_split_on_period, " ", -1); // Split on whitespace
g_free(fn_last_split_on_period);
gchar * last_item = koto_utils_string_replace_all(whitespace_split[g_strv_length(split) - 1], "#", ""); // Get last item, removing any # from it
g_strfreev(whitespace_split);
gchar ** hyphen_split = g_strsplit(last_item, "-", -1); // Split on hyphen
g_free(last_item);
gchar * position_str = NULL;
for (guint i = 0; i < g_strv_length(hyphen_split); i++) { // Iterate over each item
gchar * pos_str = hyphen_split[i];
if (!g_regex_match(num_pat, pos_str, 0, NULL)) { // Is not a number
continue;
}
position_str = g_strdup(pos_str);
break;
}
g_strfreev(hyphen_split);
guint64 position = 0;
if (position_str != NULL) { // If we have a string defined
if (g_regex_match(num_pat, position_str, 0, NULL)) { // Matches being a number
position = g_ascii_strtoull(position_str, NULL, 10); // Attempt to convert
}
g_free(position_str);
}
g_free(num_pat);
return position;
}
gint koto_track_helpers_sort_tracks(
gconstpointer track1,
gconstpointer track2,
gpointer user_data
) {
(void) user_data;
KotoTrack * track1_real = (KotoTrack*) track1;
KotoTrack * track2_real = (KotoTrack*) track2;
if (!KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Neither tracks actually exist
return 0;
} else if (KOTO_IS_TRACK(track1_real) && !KOTO_IS_TRACK(track2_real)) { // Only track2 does not exist
return -1;
} else if (!KOTO_IS_TRACK(track1_real) && KOTO_IS_TRACK(track2_real)) { // Only track1 does not exist
return 1;
}
guint track1_disc = koto_track_get_disc_number(track1_real);
guint track2_disc = koto_track_get_disc_number(track2_real);
if (track1_disc < track2_disc) { // Track 2 is in a later CD / Disc
return -1;
} else if (track1_disc > track2_disc) { // Track1 is later
return 1;
}
guint track1_pos = koto_track_get_position(track1_real);
guint track2_pos = koto_track_get_position(track2_real);
if (track1_pos == track2_pos) { // Identical positions (like reported as 0)
return g_utf8_collate(koto_track_get_name(track1_real), koto_track_get_name(track2_real));
} else if (track1_pos < track2_pos) {
return -1;
} else {
return 1;
}
}
gint koto_track_helpers_sort_track_items(
gconstpointer track1_item,
gconstpointer track2_item,
gpointer user_data
) {
KotoTrack * track1 = koto_track_item_get_track((KotoTrackItem*) track1_item);
KotoTrack * track2 = koto_track_item_get_track((KotoTrackItem*) track2_item);
return koto_track_helpers_sort_tracks(track1, track2, user_data);
}
gint koto_track_helpers_sort_tracks_by_uuid(
gconstpointer track1_uuid,
gconstpointer track2_uuid,
gpointer user_data
) {
KotoTrack * track1 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track1_uuid);
KotoTrack * track2 = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) track2_uuid);
return koto_track_helpers_sort_tracks(track1, track2, user_data);
}

View file

@ -1,49 +0,0 @@
/* track-helpers.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
void koto_track_helpers_init();
guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name);
gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre);
gchar * koto_track_helpers_get_name_for_file(
const gchar * path,
gchar * optional_artist_name
);
guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name);
gint koto_track_helpers_sort_track_items(
gconstpointer track1_item,
gconstpointer track2_item,
gpointer user_data
);
gint koto_track_helpers_sort_tracks(
gconstpointer track1,
gconstpointer track2,
gpointer user_data
);
gint koto_track_helpers_sort_tracks_by_uuid(
gconstpointer track1_uuid,
gconstpointer track2_uuid,
gpointer user_data
);

View file

@ -1,879 +0,0 @@
/* track.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <sqlite3.h>
#include <taglib/tag_c.h>
#include "../db/db.h"
#include "../db/cartographer.h"
#include "structs.h"
#include "track-helpers.h"
#include "koto-utils.h"
extern KotoCartographer * koto_maps;
extern sqlite3 * koto_db;
struct _KotoTrack {
GObject parent_instance;
gchar * artist_uuid;
gchar * album_uuid;
gchar * uuid;
GHashTable * paths;
gchar * parsed_name;
guint cd;
guint64 position;
guint64 duration;
gchar * description;
gchar * narrator;
guint64 playback_position;
guint64 year;
GList * genres;
gboolean do_initial_index;
};
G_DEFINE_TYPE(KotoTrack, koto_track, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_ARTIST_UUID,
PROP_ALBUM_UUID,
PROP_UUID,
PROP_DO_INITIAL_INDEX,
PROP_PARSED_NAME,
PROP_CD,
PROP_POSITION,
PROP_DURATION,
PROP_DESCRIPTION,
PROP_NARRATOR,
PROP_YEAR,
PROP_PLAYBACK_POSITION,
PROP_PREPARSED_GENRES,
N_PROPERTIES
};
static GParamSpec * props[N_PROPERTIES] = {
NULL
};
static void koto_track_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_track_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_track_class_init(KotoTrackClass * c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_track_set_property;
gobject_class->get_property = koto_track_get_property;
props[PROP_ARTIST_UUID] = g_param_spec_string(
"artist-uuid",
"UUID to Artist associated with the File",
"UUID to Artist associated with the File",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_ALBUM_UUID] = g_param_spec_string(
"album-uuid",
"UUID to Album associated with the File",
"UUID to Album associated with the File",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_UUID] = g_param_spec_string(
"uuid",
"UUID to File in database",
"UUID to File in database",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_DO_INITIAL_INDEX] = g_param_spec_boolean(
"do-initial-index",
"Do an initial indexing operating instead of pulling from the database",
"Do an initial indexing operating instead of pulling from the database",
FALSE,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_PARSED_NAME] = g_param_spec_string(
"parsed-name",
"Parsed Name of File",
"Parsed Name of File",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_CD] = g_param_spec_uint(
"cd",
"CD the Track belongs to",
"CD the Track belongs to",
0,
G_MAXUINT16,
1,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_POSITION] = g_param_spec_uint64(
"position",
"Position in Audiobook, Album, etc.",
"Position in Audiobook, Album, etc.",
0,
G_MAXUINT64,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_DURATION] = g_param_spec_uint64(
"duration",
"Duration of Track",
"Duration of Track",
0,
G_MAXUINT64,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_DESCRIPTION] = g_param_spec_string(
"description",
"Description of Track, typically show notes for a podcast episode",
"Description of Track, typically show notes for a podcast episode",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_NARRATOR] = g_param_spec_string(
"narrator",
"Narrator, typically of an Audiobook",
"Narrator, typically of an Audiobook",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_YEAR] = g_param_spec_uint64(
"year",
"Year",
"Year",
0,
G_MAXUINT64,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_PLAYBACK_POSITION] = g_param_spec_uint64(
"playback-position",
"Current playback position",
"Current playback position",
0,
G_MAXUINT64,
0,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
props[PROP_PREPARSED_GENRES] = g_param_spec_string(
"preparsed-genres",
"Preparsed Genres",
"Preparsed Genres",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_track_init(KotoTrack * self) {
self->description = NULL; // Initialize our description
self->duration = 0; // Initialize our duration
self->genres = NULL; // Initialize our genres list
self->narrator = NULL, // Initialize our narrator
self->paths = g_hash_table_new(g_str_hash, g_str_equal); // Create our hash table of paths
self->position = 0; // Initialize our duration
self->year = 0; // Initialize our year
}
static void koto_track_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoTrack * self = KOTO_TRACK(obj);
switch (prop_id) {
case PROP_ARTIST_UUID:
g_value_set_string(val, self->artist_uuid);
break;
case PROP_ALBUM_UUID:
g_value_set_string(val, self->album_uuid);
break;
case PROP_UUID:
g_value_set_string(val, self->uuid);
break;
case PROP_PARSED_NAME:
g_value_set_string(val, self->parsed_name);
break;
case PROP_CD:
g_value_set_uint(val, self->cd);
break;
case PROP_DESCRIPTION:
g_value_set_string(val, self->description);
break;
case PROP_NARRATOR:
g_value_set_string(val, self->narrator);
break;
case PROP_YEAR:
g_value_set_uint64(val, self->year);
break;
case PROP_POSITION:
g_value_set_uint64(val, self->position);
break;
case PROP_PLAYBACK_POSITION:
g_value_set_uint64(val, koto_track_get_playback_position(self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_track_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoTrack * self = KOTO_TRACK(obj);
switch (prop_id) {
case PROP_ARTIST_UUID:
self->artist_uuid = g_strdup(g_value_get_string(val));
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_UUID]);
break;
case PROP_ALBUM_UUID:
koto_track_set_album_uuid(self, g_value_get_string(val));
break;
case PROP_UUID:
self->uuid = g_strdup(g_value_get_string(val));
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]);
break;
case PROP_DO_INITIAL_INDEX:
self->do_initial_index = g_value_get_boolean(val);
break;
case PROP_PARSED_NAME:
koto_track_set_parsed_name(self, g_strdup(g_value_get_string(val)));
break;
case PROP_CD:
koto_track_set_cd(self, g_value_get_uint(val));
break;
case PROP_POSITION:
koto_track_set_position(self, g_value_get_uint64(val));
break;
case PROP_PLAYBACK_POSITION:
koto_track_set_playback_position(self, g_value_get_uint64(val));
break;
case PROP_DURATION:
koto_track_set_duration(self, g_value_get_uint64(val));
break;
case PROP_DESCRIPTION:
koto_track_set_description(self, g_value_get_string(val));
break;
case PROP_NARRATOR:
koto_track_set_narrator(self, g_strdup(g_value_get_string(val)));
break;
case PROP_YEAR:
koto_track_set_year(self, g_value_get_uint64(val));
break;
case PROP_PREPARSED_GENRES:
koto_track_set_preparsed_genres(self, g_strdup(g_value_get_string(val)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_track_commit(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(self->artist_uuid)) { // No valid required artist UUID
return;
}
gchar * commit_msg = "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres)" \
"VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d, %d, '%s')" \
"ON CONFLICT(id) DO UPDATE SET album_id=excluded.album_id, artist_id=excluded.artist_id, name=excluded.name, disc=excluded.disc, position=excluded.position, duration=excluded.duration, genres=excluded.genres;";
// Combine our list items into a semi-colon separated string
gchar * genres = koto_utils_join_string_list(self->genres, ";"); // Join our GList of strings into a single
gchar * commit_op = g_strdup_printf(
commit_msg,
self->uuid,
self->artist_uuid,
koto_utils_string_get_valid(self->album_uuid),
g_strescape(self->parsed_name, NULL),
(int) self->cd,
(int) self->position,
(int) self->duration,
genres
);
if (new_transaction(commit_op, "Failed to write our file to the database", FALSE) != SQLITE_OK) {
return;
}
g_free(genres); // Free the genres string
GHashTableIter paths_iter;
g_hash_table_iter_init(&paths_iter, self->paths); // Create an iterator for our paths
gpointer lib_uuid_ptr, track_rel_path_ptr;
while (g_hash_table_iter_next(&paths_iter, &lib_uuid_ptr, &track_rel_path_ptr)) {
gchar * lib_uuid = lib_uuid_ptr;
gchar * track_rel_path = track_rel_path_ptr;
gchar * commit_op = g_strdup_printf(
"INSERT INTO libraries_tracks(id, track_id, path)"
"VALUES ('%s', '%s', quote(\"%s\"))"
"ON CONFLICT(id, track_id) DO UPDATE SET path=excluded.path;",
lib_uuid,
self->uuid,
track_rel_path
);
new_transaction(commit_op, "Failed to add this path for the track", FALSE);
}
}
gchar * koto_track_get_description(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? g_strdup(self->description) : NULL;
}
guint koto_track_get_disc_number(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->cd : 1;
}
guint64 koto_track_get_duration(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->duration : 0;
}
GList * koto_track_get_genres(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->genres : NULL;
}
GVariant * koto_track_get_metadata_vardict(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) {
return NULL;
}
GVariantBuilder * builder = g_variant_builder_new(G_VARIANT_TYPE_VARDICT);
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid);
gchar * artist_name = koto_artist_get_name(artist);
if (koto_utils_string_is_valid(self->album_uuid)) { // Have an album associated
KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid);
if (KOTO_IS_ALBUM(album)) {
gchar * album_art_path = koto_album_get_art(album);
gchar * album_name = koto_album_get_name(album);
if (koto_utils_string_is_valid(album_art_path)) { // Valid album art path
album_art_path = g_strconcat("file://", album_art_path, NULL); // Prepend with file://
g_variant_builder_add(builder, "{sv}", "mpris:artUrl", g_variant_new_string(album_art_path));
}
g_variant_builder_add(builder, "{sv}", "xesam:album", g_variant_new_string(album_name));
}
} else {
} // TODO: Implement artist artwork fetching here
g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(self->uuid));
if (koto_utils_string_is_valid(artist_name)) { // Valid artist name
GVariant * artist_name_variant;
GVariantBuilder * artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as"));
g_variant_builder_add(artist_list_builder, "s", artist_name);
artist_name_variant = g_variant_new("as", artist_list_builder);
g_variant_builder_unref(artist_list_builder);
g_variant_builder_add(builder, "{sv}", "xesam:artist", artist_name_variant);
g_variant_builder_add(builder, "{sv}", "playbackengine:artist", g_variant_new_string(artist_name)); // Add a sort of "meta" string val for our playback engine so we don't need to mess about with the array
}
g_variant_builder_add(builder, "{sv}", "xesam:discNumber", g_variant_new_uint64(self->cd));
g_variant_builder_add(builder, "{sv}", "xesam:title", g_variant_new_string(self->parsed_name));
g_variant_builder_add(builder, "{sv}", "xesam:url", g_variant_new_string(koto_track_get_path(self)));
g_variant_builder_add(builder, "{sv}", "xesam:trackNumber", g_variant_new_uint64(self->position));
GVariant * metadata_ret = g_variant_builder_end(builder);
return metadata_ret;
}
gchar * koto_track_get_name(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? g_strdup(self->parsed_name) : NULL;
}
gchar * koto_track_get_narrator(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? g_strdup(self->narrator) : NULL;
}
gchar * koto_track_get_path(KotoTrack * self) {
if (!KOTO_IS_TRACK(self) || (KOTO_IS_TRACK(self) && (g_list_length(g_hash_table_get_keys(self->paths)) == 0))) { // If this is not a track or is but we have no paths associated with it
return NULL;
}
GHashTableIter iter;
g_hash_table_iter_init(&iter, self->paths); // Create an iterator for our paths
gpointer uuidptr;
gpointer relpathptr;
gchar * path = NULL;
while (g_hash_table_iter_next(&iter, &uuidptr, &relpathptr)) { // Iterate over all the paths for this file
KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, (gchar*) uuidptr);
if (KOTO_IS_LIBRARY(library)) {
path = g_strdup(g_build_path(G_DIR_SEPARATOR_S, koto_library_get_path(library), koto_library_get_relative_path_to_file(library, (gchar*) relpathptr), NULL)); // Build our full library path using library's path and our file relative path
break;
}
}
return path;
}
guint64 koto_track_get_playback_position(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->playback_position : 0;
}
guint64 koto_track_get_position(KotoTrack * self) {
return KOTO_IS_TRACK(self) ? self->position : 0;
}
gchar * koto_track_get_uniqueish_key(KotoTrack * self) {
KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); // Get the artist associated with this track
if (!KOTO_IS_ARTIST(artist)) { // Don't have an artist
return g_strdup(self->parsed_name); // Just return the name of the file, which is very likely not unique
}
gchar * artist_name = koto_artist_get_name(artist); // Get the artist name
if (koto_utils_string_is_valid(self->album_uuid)) { // If we have an album associated with this track (not necessarily guaranteed)
KotoAlbum * possible_album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid);
if (KOTO_IS_ALBUM(possible_album)) { // Album exists
gchar * album_name = koto_album_get_name(possible_album); // Get the name of the album
if (koto_utils_string_is_valid(album_name)) {
return g_strdup_printf("%s-%s-%s", artist_name, album_name, self->parsed_name); // Create a key of (ARTIST/WRITER)-(ALBUM/AUDIOBOOK)-(CHAPTER/TRACK)
}
}
}
return g_strdup_printf("%s-%s", artist_name, self->parsed_name); // Create a key of just (ARTIST/WRITER)-(CHAPTER/TRACK)
}
gchar * koto_track_get_uuid(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) {
return NULL;
}
return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different
}
guint64 koto_track_get_year(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) {
return 0;
}
return self->year;
}
void koto_track_remove_from_playlist(
KotoTrack * self,
gchar * playlist_uuid
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
gchar * commit_op = g_strdup_printf(
"DELETE FROM playlist_tracks WHERE track_id='%s' AND playlist_id='%s'",
self->uuid,
playlist_uuid
);
new_transaction(commit_op, "Failed to remove track from playlist", FALSE);
}
void koto_track_set_album_uuid(
KotoTrack * self,
const gchar * album_uuid
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (album_uuid == NULL) {
return;
}
gchar * uuid = g_strdup(album_uuid);
if (!koto_utils_string_is_valid(uuid)) { // If this is not a valid string
return;
}
self->album_uuid = uuid;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ALBUM_UUID]);
}
void koto_track_save_to_playlist(
KotoTrack * self,
gchar * playlist_uuid
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
gchar * commit_op = g_strdup_printf(
"INSERT INTO playlist_tracks(playlist_id, track_id)"
"VALUES('%s', '%s')",
playlist_uuid,
self->uuid
);
new_transaction(commit_op, "Failed to save track to playlist", FALSE);
}
void koto_track_set_cd(
KotoTrack * self,
guint cd
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (cd == 0) { // No change really
return;
}
self->cd = cd;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CD]);
}
void koto_track_set_description(
KotoTrack * self,
const gchar * description
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(description)) {
return;
}
if (g_strcmp0(self->description, description) == 0) { // Same description
return;
}
if (koto_utils_string_is_valid(self->description)) {
g_free(self->description); // Free the existing narrator
}
self->description = g_strdup(description); // Duplicate our description
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]);
}
void koto_track_set_duration(
KotoTrack * self,
guint64 duration
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (duration == 0) {
return;
}
self->duration = duration;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DURATION]);
}
void koto_track_set_genres(
KotoTrack * self,
char * genrelist
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid((gchar*) genrelist)) { // If it is an empty string
return;
}
gchar ** genres = g_strsplit(genrelist, ";", -1); // Split on semicolons for each genre, e.g. Electronic;Rock
guint len = g_strv_length(genres);
if (len == 0) { // No genres
g_strfreev(genres); // Free the list
return;
}
for (guint i = 0; i < len; i++) { // Iterate over each item
gchar * genre = genres[i]; // Get the genre
gchar * lowercased_genre = g_utf8_strdown(g_strstrip(genre), -1); // Lowercase the genre
gchar * lowercased_hyphenated_genre = koto_utils_string_replace_all(lowercased_genre, " ", "-");
g_free(lowercased_genre); // Free the lowercase genre string since we no longer need it
gchar * corrected_genre = koto_track_helpers_get_corrected_genre(lowercased_hyphenated_genre); // Get any corrected genre
if (g_list_index(self->genres, corrected_genre) == -1) { // Don't have this genre added
self->genres = g_list_append(self->genres, g_strdup(corrected_genre)); // Add the genre to our list
}
g_free(lowercased_hyphenated_genre); // Free our remaining string
}
g_strfreev(genres); // Free the list of genres locally
}
void koto_track_set_narrator(
KotoTrack * self,
const gchar * narrator
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(narrator)) {
return;
}
if (g_strcmp0(self->narrator, narrator) == 0) { // Same narrator
return;
}
if (koto_utils_string_is_valid(self->narrator)) {
g_free(self->narrator); // Free the existing narrator
}
self->narrator = g_strdup(narrator); // Duplicate our narrator
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]);
}
void koto_track_set_parsed_name(
KotoTrack * self,
gchar * new_parsed_name
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(new_parsed_name)) {
return;
}
gboolean have_existing_name = koto_utils_string_is_valid(self->parsed_name);
if (have_existing_name && (strcmp(self->parsed_name, new_parsed_name) == 0)) { // Have existing name that matches one provided
return; // Don't do anything
}
if (have_existing_name) {
g_free(self->parsed_name);
}
self->parsed_name = g_strdup(new_parsed_name);
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PARSED_NAME]);
}
void koto_track_set_path(
KotoTrack * self,
KotoLibrary * lib,
gchar * fixed_path
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(fixed_path)) { // Not a valid path
return;
}
gchar * path = g_strdup(fixed_path); // Duplicate our fixed_path
gchar * relative_path = koto_library_get_relative_path_to_file(lib, path); // Get the relative path to the file for the given library
gchar * library_uuid = koto_library_get_uuid(lib); // Get the library for this path
g_hash_table_replace(self->paths, library_uuid, relative_path); // Replace any existing value or add this one
if (self->do_initial_index) {
koto_track_update_metadata(self); // Attempt to get ID3 info
}
}
void koto_track_set_playback_position(
KotoTrack * self,
guint64 position
) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return;
}
self->playback_position = position;
}
void koto_track_set_position(
KotoTrack * self,
guint64 pos
) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return;
}
if (pos == 0) { // No position change really
return;
}
self->position = pos;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_POSITION]);
}
void koto_track_set_year(
KotoTrack * self,
guint64 year
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
self->year = year;
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]);
}
void koto_track_update_metadata(KotoTrack * self) {
if (!KOTO_IS_TRACK(self)) { // Not a track
return;
}
gchar * optimal_track_path = koto_track_get_path(self); // Check all the libraries associated with this track, based on priority, return a built path using lib path + relative file path
if (!koto_utils_string_is_valid(optimal_track_path)) { // Not a valid string
return;
}
TagLib_File * t_file = taglib_file_new(optimal_track_path); // Get a taglib file for this file
if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid
TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag
koto_track_set_genres(self, taglib_tag_genre(tag)); // Set our genres to any genres listed for the track
koto_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer
koto_track_set_year(self, (guint64) taglib_tag_year(tag)); // Get the track year and convert it to guint64
const TagLib_AudioProperties * tag_props = taglib_file_audioproperties(t_file); // Get the audio properties of the file
koto_track_set_duration(self, taglib_audioproperties_length(tag_props)); // Get the length of the track and set it as our duration
}
if (self->position == 0) { // Failed to get tag info or got the tag info but position is zero
guint64 position = koto_track_helpers_get_position_based_on_file_name(g_path_get_basename(optimal_track_path)); // Get the likely position
koto_track_set_position(self, position); // Set our position
}
taglib_tag_free_strings(); // Free strings
taglib_file_free(t_file); // Free the file
g_free(optimal_track_path);
}
void koto_track_set_preparsed_genres(
KotoTrack * self,
gchar * genrelist
) {
if (!KOTO_IS_TRACK(self)) {
return;
}
if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string
return;
}
GList * preparsed_genres_list = koto_utils_string_to_string_list(genrelist, ";");
if (g_list_length(preparsed_genres_list) == 0) { // No genres
g_list_free(preparsed_genres_list);
return;
}
// TODO: Do a pass on in first memory optimization phase to ensure string elements are freed.
g_list_free_full(self->genres, NULL); // Free the existing genres list
self->genres = preparsed_genres_list;
}
KotoTrack * koto_track_new(
const gchar * artist_uuid,
const gchar * album_uuid,
const gchar * parsed_name,
guint cd
) {
KotoTrack * track = g_object_new(
KOTO_TYPE_TRACK,
"artist-uuid",
artist_uuid,
"album-uuid",
album_uuid,
"do-initial-index",
TRUE,
"uuid",
g_uuid_string_random(),
"cd",
cd,
"parsed-name",
parsed_name,
NULL
);
return track;
}
KotoTrack * koto_track_new_with_uuid(const gchar * uuid) {
return g_object_new(
KOTO_TYPE_TRACK,
"uuid",
g_strdup(uuid),
NULL
);
}

View file

@ -1,111 +0,0 @@
/* koto-dialog-container.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "components/button.h"
#include "koto-dialog-container.h"
struct _KotoDialogContainer {
GtkBox parent_instance;
KotoButton * close_button;
GtkWidget * dialogs;
};
G_DEFINE_TYPE(KotoDialogContainer, koto_dialog_container, GTK_TYPE_BOX);
static void koto_dialog_container_class_init(KotoDialogContainerClass * c) {
(void) c;
}
static void koto_dialog_container_init(KotoDialogContainer * self) {
gtk_widget_add_css_class(GTK_WIDGET(self), "koto-dialog-container");
g_object_set(GTK_WIDGET(self), "hexpand", TRUE, "vexpand", TRUE, NULL);
self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_LARGE);
gtk_widget_set_halign(GTK_WIDGET(self->close_button), GTK_ALIGN_END);
gtk_box_prepend(GTK_BOX(self), GTK_WIDGET(self->close_button)); // Add our close button
self->dialogs = gtk_stack_new();
gtk_stack_set_transition_duration(GTK_STACK(self->dialogs), 0); // No transition timing
gtk_stack_set_transition_type(GTK_STACK(self->dialogs), GTK_STACK_TRANSITION_TYPE_NONE); // No transition
gtk_widget_set_halign(self->dialogs, GTK_ALIGN_CENTER);
gtk_widget_set_hexpand(self->dialogs, TRUE);
gtk_widget_set_valign(self->dialogs, GTK_ALIGN_CENTER);
gtk_widget_set_vexpand(self->dialogs, TRUE);
gtk_box_append(GTK_BOX(self), self->dialogs); // Add the dialogs stack
koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_dialog_container_handle_close_click), self);
gtk_widget_hide(GTK_WIDGET(self)); // Hide by default
}
void koto_dialog_container_add_dialog(
KotoDialogContainer * self,
gchar * dialog_name,
GtkWidget * dialog
) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_stack_add_named(GTK_STACK(self->dialogs), dialog, dialog_name); // Add the dialog to the stack
}
void koto_dialog_container_handle_close_click(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
koto_dialog_container_hide((KotoDialogContainer*) user_data);
}
void koto_dialog_container_hide(KotoDialogContainer * self) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_widget_hide(GTK_WIDGET(self));
}
void koto_dialog_container_show_dialog(
KotoDialogContainer * self,
gchar * dialog_name
) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_stack_set_visible_child_name(GTK_STACK(self->dialogs), dialog_name); // Set to the dialog name
gtk_widget_show(GTK_WIDGET(self)); // Ensure we show self
}
KotoDialogContainer * koto_dialog_container_new() {
return g_object_new(
KOTO_TYPE_DIALOG_CONTAINER,
"orientation",
GTK_ORIENTATION_VERTICAL,
NULL
);
}

View file

@ -1,59 +0,0 @@
/* koto-dialog-container.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_DIALOG_CONTAINER koto_dialog_container_get_type()
G_DECLARE_FINAL_TYPE(KotoDialogContainer, koto_dialog_container, KOTO, DIALOG_CONTAINER, GtkBox);
#define KOTO_IS_DIALOG_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_DIALOG_CONTAINER))
/**
* Functions
**/
KotoDialogContainer * koto_dialog_container_new();
void koto_dialog_container_add_dialog(
KotoDialogContainer * self,
gchar * dialog_name,
GtkWidget * dialog
);
void koto_dialog_container_handle_close_click(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_dialog_container_hide(KotoDialogContainer * self);
void koto_dialog_container_show_dialog(
KotoDialogContainer * self,
gchar * dialog_name
);
G_END_DECLS

View file

@ -1,323 +0,0 @@
/* koto-expander.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib/gi18n.h>
#include <gtk-4.0/gtk/gtk.h>
#include "components/button.h"
#include "config/config.h"
#include "koto-expander.h"
#include "koto-utils.h"
enum {
PROP_EXP_0,
PROP_HEADER_ICON_NAME,
PROP_HEADER_LABEL,
PROP_HEADER_SECONDARY_BUTTON,
PROP_CONTENT,
N_EXP_PROPERTIES
};
static GParamSpec * expander_props[N_EXP_PROPERTIES] = {
NULL,
};
struct _KotoExpander {
GtkBox parent_instance;
gboolean constructed;
GtkWidget * header;
KotoButton * header_button;
gchar * icon_name;
gchar * label;
KotoButton * header_secondary_button;
KotoButton * header_expand_button;
GtkWidget * revealer;
GtkWidget * content;
};
struct _KotoExpanderClass {
GtkBoxClass parent_class;
};
G_DEFINE_TYPE(KotoExpander, koto_expander, GTK_TYPE_BOX);
static void koto_expander_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_expander_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_expander_class_init(KotoExpanderClass * c) {
GObjectClass * gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_expander_set_property;
gobject_class->get_property = koto_expander_get_property;
expander_props[PROP_HEADER_ICON_NAME] = g_param_spec_string(
"icon-name",
"Icon Name",
"Name of the icon to use in the Expander",
"emblem-favorite-symbolic",
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
expander_props[PROP_HEADER_LABEL] = g_param_spec_string(
"label",
"Label",
"Label for the Expander",
NULL,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
expander_props[PROP_HEADER_SECONDARY_BUTTON] = g_param_spec_object(
"secondary-button",
"Secondary Button",
"Secondary Button to be placed next to Expander button",
KOTO_TYPE_BUTTON,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
expander_props[PROP_CONTENT] = g_param_spec_object(
"content",
"Content",
"Content inside the Expander",
GTK_TYPE_WIDGET,
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_EXP_PROPERTIES, expander_props);
}
static void koto_expander_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoExpander * self = KOTO_EXPANDER(obj);
switch (prop_id) {
case PROP_HEADER_ICON_NAME:
g_value_set_string(val, self->icon_name);
break;
case PROP_HEADER_LABEL:
g_value_set_string(val, self->label);
break;
case PROP_HEADER_SECONDARY_BUTTON:
g_value_set_object(val, (GObject*) self->header_secondary_button);
break;
case PROP_CONTENT:
g_value_set_object(val, (GObject*) self->content);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_expander_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoExpander * self = KOTO_EXPANDER(obj);
switch (prop_id) {
case PROP_HEADER_ICON_NAME:
koto_expander_set_icon_name(self, g_strdup(g_value_get_string(val)));
break;
case PROP_HEADER_LABEL:
g_return_if_fail(GTK_IS_WIDGET(self->header_button));
koto_button_set_text(self->header_button, g_strdup(g_value_get_string(val)));
break;
case PROP_HEADER_SECONDARY_BUTTON:
koto_expander_set_secondary_button(self, (KotoButton*) g_value_get_object(val));
break;
case PROP_CONTENT:
koto_expander_set_content(self, (GtkWidget*) g_value_get_object(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_expander_init(KotoExpander * self) {
GtkStyleContext * style = gtk_widget_get_style_context(GTK_WIDGET(self));
gtk_style_context_add_class(style, "expander");
gtk_widget_set_hexpand((GTK_WIDGET(self)), TRUE);
self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
GtkStyleContext * header_style = gtk_widget_get_style_context(self->header);
gtk_style_context_add_class(header_style, "expander-header");
self->revealer = gtk_revealer_new();
gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), TRUE); // Set to be revealed by default
gtk_revealer_set_transition_type(GTK_REVEALER(self->revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
KotoButton * new_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
if (KOTO_IS_BUTTON(new_button)) { // Created our widget successfully
self->header_button = new_button;
gtk_widget_set_hexpand(GTK_WIDGET(self->header_button), TRUE);
gtk_box_prepend(GTK_BOX(self->header), GTK_WIDGET(self->header_button));
}
self->header_expand_button = koto_button_new_with_icon("", "pan-down-symbolic", "pan-up-symbolic", KOTO_BUTTON_PIXBUF_SIZE_SMALL);
gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->header_expand_button));
gtk_box_prepend(GTK_BOX(self), self->header);
gtk_box_append(GTK_BOX(self), self->revealer);
self->constructed = TRUE;
koto_button_add_click_handler(self->header_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self);
koto_button_add_click_handler(self->header_expand_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self);
}
// koto_expander_set_icon_name will set the icon for our inner KotoButton for this Expander
void koto_expander_set_icon_name(
KotoExpander * self,
gchar * icon
) {
if (!KOTO_IS_EXPANDER(self)) { // Not a KotoExpander
return;
}
if (!koto_utils_string_is_valid(icon)) { // Not a valid string
return;
}
if (!KOTO_IS_BUTTON(self->header_button)) { // Don't have a header button for whatever reason
return;
}
koto_button_set_icon_name(self->header_button, icon, FALSE);
}
void koto_expander_set_secondary_button(
KotoExpander * self,
KotoButton * new_button
) {
if (!self->constructed) {
return;
}
if (!GTK_IS_WIDGET(new_button)) {
return;
}
if (GTK_IS_WIDGET(self->header_secondary_button)) { // Already have a button
gtk_box_remove(GTK_BOX(self->header), GTK_WIDGET(self->header_secondary_button));
}
self->header_secondary_button = new_button;
gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->header_secondary_button));
g_object_notify_by_pspec(G_OBJECT(self), expander_props[PROP_HEADER_SECONDARY_BUTTON]);
}
void koto_expander_set_content(
KotoExpander * self,
GtkWidget * new_content
) {
if (!self->constructed) {
return;
}
if (GTK_IS_WIDGET(self->content)) { // Already have content
gtk_revealer_set_child(GTK_REVEALER(self->revealer), NULL);
g_object_unref(self->content);
}
self->content = new_content;
gtk_revealer_set_child(GTK_REVEALER(self->revealer), self->content);
g_object_notify_by_pspec(G_OBJECT(self), expander_props[PROP_CONTENT]);
}
GtkWidget * koto_expander_get_content(KotoExpander * self) {
return self->content;
}
void koto_expander_toggle_content(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
KotoExpander * self = data;
koto_button_flip(KOTO_BUTTON(self->header_expand_button));
GtkRevealer * rev = GTK_REVEALER(self->revealer);
gtk_revealer_set_reveal_child(rev, !gtk_revealer_get_reveal_child(rev)); // Invert our values
}
KotoExpander * koto_expander_new(
gchar * primary_icon_name,
gchar * primary_label_text
) {
return g_object_new(
KOTO_TYPE_EXPANDER,
"orientation",
GTK_ORIENTATION_VERTICAL,
"icon-name",
primary_icon_name,
"label",
primary_label_text,
NULL
);
}
KotoExpander * koto_expander_new_with_button(
gchar * primary_icon_name,
gchar * primary_label_text,
KotoButton * secondary_button
) {
return g_object_new(
KOTO_TYPE_EXPANDER,
"orientation",
GTK_ORIENTATION_VERTICAL,
"icon-name",
primary_icon_name,
"label",
primary_label_text,
"secondary-button",
secondary_button,
NULL
);
}

View file

@ -1,66 +0,0 @@
/* koto-expander.h
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <gtk-4.0/gtk/gtk.h>
#include "components/button.h"
G_BEGIN_DECLS
#define KOTO_TYPE_EXPANDER (koto_expander_get_type())
G_DECLARE_FINAL_TYPE(KotoExpander, koto_expander, KOTO, EXPANDER, GtkBox)
#define KOTO_IS_EXPANDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_EXPANDER))
KotoExpander * koto_expander_new(gchar * primary_icon_name, gchar * primary_label_text);
KotoExpander * koto_expander_new_with_button(
gchar * primary_icon_name,
gchar * primary_label_text,
KotoButton * secondary_button
);
GtkWidget * koto_expander_get_content(KotoExpander * self);
void koto_expander_set_icon_name(
KotoExpander * self,
gchar * icon
);
void koto_expander_set_label(
KotoExpander * self,
const gchar * label
);
void koto_expander_set_secondary_button(
KotoExpander * self,
KotoButton * new_button
);
void koto_expander_set_content(
KotoExpander * self,
GtkWidget * new_content
);
void koto_expander_toggle_content(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer data
);
G_END_DECLS

View file

@ -1,40 +0,0 @@
?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkImage" id="audioHeadphonesMenuImage">
<property name="can-focus">False</property>
<property name="icon-name">audio-headphones</property>
<property name="icon_size">3</property>
</object>
<template class="GtkHeaderBar" parent="GtkHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<property name="decoration-layout">appmenu:minimize,maximize,close</property>
<child type="title">
<object class="GtkSearchEntry" id="search">
<property name="width-request">400</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Search...</property>
<property name="input-hints">GTK_INPUT_HINT_NO_EMOJI | GTK_INPUT_HINT_NONE</property>
</object>
</child>
<child>
<object class="GtkButton" id="menu_button">
<property name="name">menu_button</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="image">audioHeadphonesMenuImage</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</template>
</interface>

View file

@ -1,318 +0,0 @@
/* koto-nav.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtk-4.0/gtk/gtk.h>
#include "components/button.h"
#include "config/config.h"
#include "db/cartographer.h"
#include "indexer/structs.h"
#include "playlist/playlist.h"
#include "koto-expander.h"
#include "koto-nav.h"
#include "koto-utils.h"
#include "koto-window.h"
extern KotoCartographer * koto_maps;
extern KotoWindow * main_window;
struct _KotoNav {
GObject parent_instance;
GtkWidget * win;
GtkWidget * content;
KotoButton * home_button;
KotoExpander * audiobook_expander;
KotoExpander * music_expander;
KotoExpander * podcast_expander;
KotoExpander * playlists_expander;
// Audiobooks
KotoButton * audiobooks_local;
KotoButton * audiobooks_audible;
KotoButton * audiobooks_librivox;
// Music
KotoButton * music_local;
KotoButton * music_radio;
// Playlists
GHashTable * playlist_buttons;
// Podcasts
KotoButton * podcasts_local;
KotoButton * podcasts_discover;
};
struct _KotoNavClass {
GObjectClass parent_class;
};
G_DEFINE_TYPE(KotoNav, koto_nav, G_TYPE_OBJECT);
static void koto_nav_class_init(KotoNavClass * c) {
(void) c;
}
static void koto_nav_init(KotoNav * self) {
self->playlist_buttons = g_hash_table_new(g_str_hash, g_str_equal);
self->win = gtk_scrolled_window_new();
gtk_widget_set_hexpand_set(self->win, TRUE); // using hexpand-set works, hexpand seems to break it by causing it to take up way too much space
gtk_widget_set_size_request(self->win, 300, -1);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->win), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(self->win), 300);
gtk_scrolled_window_set_max_content_width(GTK_SCROLLED_WINDOW(self->win), 300);
gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(self->win), TRUE);
self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_add_css_class(self->win, "primary-nav");
gtk_widget_set_vexpand(self->win, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->win), self->content);
KotoButton * h_button = koto_button_new_with_icon("Home", "user-home-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
if (h_button != NULL) {
self->home_button = h_button;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->home_button));
}
koto_nav_create_audiobooks_section(self);
koto_nav_create_music_section(self);
koto_nav_create_podcasts_section(self);
koto_nav_create_playlist_section(self);
}
void koto_nav_create_audiobooks_section(KotoNav * self) {
KotoExpander * a_expander = koto_expander_new("ephy-bookmarks-symbolic", "Audiobooks");
self->audiobook_expander = a_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->audiobook_expander));
GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
koto_expander_set_content(a_expander, new_content);
self->audiobooks_local = koto_button_new_plain("Library");
koto_button_set_data(self->audiobooks_local, &"audiobooks.library");
self->audiobooks_audible = koto_button_new_plain("Audible");
self->audiobooks_librivox = koto_button_new_plain("LibriVox");
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_local));
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_audible));
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_librivox));
koto_button_add_click_handler(self->audiobooks_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), self->audiobooks_local);
}
void koto_nav_create_music_section(KotoNav * self) {
KotoExpander * m_expander = koto_expander_new("emblem-music-symbolic", "Music");
self->music_expander = m_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->music_expander));
GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
self->music_local = koto_button_new_plain("Library");
koto_button_set_data(self->music_local, &"music.library");
self->music_radio = koto_button_new_plain("Radio");
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_local));
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_radio));
koto_expander_set_content(m_expander, new_content);
koto_button_add_click_handler(self->music_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), self->music_local);
}
void koto_nav_create_playlist_section(KotoNav * self) {
KotoButton * playlist_add_button = koto_button_new_with_icon("", "list-add-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
KotoExpander * pl_expander = koto_expander_new_with_button("playlist-symbolic", "Playlists", playlist_add_button);
self->playlists_expander = pl_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->playlists_expander));
// TODO: Turn into ListBox to sort playlists
GtkWidget * playlist_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
koto_expander_set_content(self->playlists_expander, playlist_list);
koto_button_add_click_handler(playlist_add_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_playlist_add_click), NULL);
g_signal_connect(koto_maps, "playlist-added", G_CALLBACK(koto_nav_handle_playlist_added), self);
g_signal_connect(koto_maps, "playlist-removed", G_CALLBACK(koto_nav_handle_playlist_removed), self);
}
void koto_nav_create_podcasts_section(KotoNav * self) {
KotoExpander * p_expander = koto_expander_new("microphone-sensitivity-high-symbolic", "Podcasts");
self->podcast_expander = p_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->podcast_expander));
GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
self->podcasts_local = koto_button_new_plain("Library");
self->podcasts_discover = koto_button_new_plain("Find New Podcasts");
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->podcasts_local));
gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->podcasts_discover));
koto_expander_set_content(p_expander, new_content);
}
void koto_nav_handle_playlist_add_click(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
) {
(void) gesture;
(void) n_press;
(void) x;
(void) y;
(void) user_data;
koto_window_show_dialog(main_window, "create-modify-playlist");
}
void koto_nav_handle_playlist_added(
KotoCartographer * carto,
KotoPlaylist * playlist,
gpointer user_data
) {
(void) carto;
if (!KOTO_IS_PLAYLIST(playlist)) {
return;
}
if (koto_playlist_get_is_hidden(playlist)) { // Should be hidden from nav
return;
}
KotoNav * self = user_data;
if (!KOTO_IS_NAV(self)) {
return;
}
gchar * playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID for a playlist
if (g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Already added button
g_free(playlist_uuid);
return;
}
gchar * playlist_name = koto_playlist_get_name(playlist);
gchar * playlist_art_path = koto_playlist_get_artwork(playlist); // Get any file path for it
KotoButton * playlist_button = NULL;
if (koto_utils_string_is_valid(playlist_art_path)) { // Have a file associated
playlist_button = koto_button_new_with_file(playlist_name, playlist_art_path, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
} else { // No file associated
playlist_button = koto_button_new_with_icon(playlist_name, "audio-x-generic-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
}
if (!KOTO_IS_BUTTON(playlist_button)) { // Failed to create the playlist button
return;
}
koto_button_set_data(playlist_button, playlist_uuid); // Set our data to the playlist UUID string
g_hash_table_insert(self->playlist_buttons, playlist_uuid, playlist_button); // Add the button
// TODO: Make this a ListBox and sort the playlists alphabetically
GtkBox * playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander));
if (GTK_IS_BOX(playlist_expander_content)) {
gtk_box_append(playlist_expander_content, GTK_WIDGET(playlist_button));
koto_button_add_click_handler(playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), playlist_button);
koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS
g_signal_connect(playlist, "modified", G_CALLBACK(koto_nav_handle_playlist_modified), self);
}
}
void koto_nav_handle_playlist_modified(
KotoPlaylist * playlist,
gpointer user_data
) {
if (!KOTO_IS_PLAYLIST(playlist)) {
return;
}
KotoNav * self = user_data;
if (!KOTO_IS_NAV(self)) {
return;
}
gchar * playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID for a playlist
KotoButton * playlist_button = g_hash_table_lookup(self->playlist_buttons, playlist_uuid);
if (!KOTO_IS_BUTTON(playlist_button)) {
return;
}
gchar * artwork = koto_playlist_get_artwork(playlist); // Get the artwork
if (koto_utils_string_is_valid(artwork)) { // Have valid artwork
koto_button_set_file_path(playlist_button, artwork); // Update the artwork path
}
gchar * name = koto_playlist_get_name(playlist); // Get the name
if (koto_utils_string_is_valid(name)) { // Have valid name
koto_button_set_text(playlist_button, name); // Update the button text
}
}
void koto_nav_handle_playlist_removed(
KotoCartographer * carto,
gchar * playlist_uuid,
gpointer user_data
) {
(void) carto;
KotoNav * self = user_data;
if (!g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Does not contain this
return;
}
KotoButton * playlist_btn = g_hash_table_lookup(self->playlist_buttons, playlist_uuid); // Get the playlist button
if (!KOTO_IS_BUTTON(playlist_btn)) { // Not a playlist button
return;
}
GtkBox * playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander));
gtk_box_remove(playlist_expander_content, GTK_WIDGET(playlist_btn)); // Remove the button
g_hash_table_remove(self->playlist_buttons, playlist_uuid); // Remove from the playlist buttons hash table
}
GtkWidget * koto_nav_get_nav(KotoNav * self) {
return self->win;
}
KotoNav * koto_nav_new(void) {
return g_object_new(KOTO_TYPE_NAV, NULL);
}

View file

@ -1,65 +0,0 @@
/* koto-nav.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "indexer/structs.h"
G_BEGIN_DECLS
#define KOTO_TYPE_NAV (koto_nav_get_type())
G_DECLARE_FINAL_TYPE(KotoNav, koto_nav, KOTO, NAV, GObject)
KotoNav* koto_nav_new(void);
void koto_nav_create_audiobooks_section(KotoNav * self);
void koto_nav_create_music_section(KotoNav * self);
void koto_nav_create_playlist_section(KotoNav * self);
void koto_nav_create_podcasts_section(KotoNav * self);
void koto_nav_handle_playlist_add_click(
GtkGestureClick * gesture,
int n_press,
double x,
double y,
gpointer user_data
);
void koto_nav_handle_playlist_added(
KotoCartographer * carto,
KotoPlaylist * playlist,
gpointer user_data
);
void koto_nav_handle_playlist_modified(
KotoPlaylist * playlist,
gpointer user_data
);
void koto_nav_handle_playlist_removed(
KotoCartographer * carto,
gchar * playlist_uuid,
gpointer user_data
);
GtkWidget * koto_nav_get_nav(KotoNav * self);
G_END_DECLS

Some files were not shown because too many files have changed in this diff Show more