├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── deploy-docs.yml
├── .gitignore
├── .spi.yml
├── .swiftlint.yml
├── .swiftpm
├── configuration
│ └── Package.resolved
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ ├── MusadoraKit.xcscheme
│ └── MusadoraKitTests.xcscheme
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Musadora
├── Musadora.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Musadora.xcscheme
├── Musadora
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── Group 121.png
│ │ └── Contents.json
│ ├── Catalog
│ │ ├── CatalogView.swift
│ │ └── Station Genres
│ │ │ ├── StationGenreDetailedView.swift
│ │ │ └── StationGenresView.swift
│ ├── Charts
│ │ ├── AlbumChartView.swift
│ │ ├── ChartView.swift
│ │ ├── ChartsView.swift
│ │ ├── MusicVideoChartView.swift
│ │ ├── PlaylistChartView.swift
│ │ └── SongChartView.swift
│ ├── History
│ │ ├── MusicSummariesView.swift
│ │ ├── RecentlyAddedView.swift
│ │ └── RecentlyPlayedView.swift
│ ├── Info.plist
│ ├── Library
│ │ ├── LibraryAlbumsView.swift
│ │ ├── LibraryPlaylistsView.swift
│ │ ├── LibrarySongsView.swift
│ │ └── LibraryView.swift
│ ├── Main
│ │ ├── MTabView.swift
│ │ └── MusadoraApp.swift
│ ├── Music Items
│ │ ├── Album
│ │ │ ├── AlbumRow.swift
│ │ │ └── AlbumsView.swift
│ │ ├── Playlist
│ │ │ ├── PlaylistRow.swift
│ │ │ └── PlaylistsView.swift
│ │ ├── Song
│ │ │ ├── SongRow.swift
│ │ │ └── SongsView.swift
│ │ └── Station
│ │ │ └── StationRow.swift
│ ├── Onboarding
│ │ └── WelcomeView.swift
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── README.md
│ ├── Recommendations
│ │ ├── RecommendationView.swift
│ │ └── RecommendationsView.swift
│ ├── Settings
│ │ └── SettingsView.swift
│ └── Shared
│ │ └── NavigationListStack.swift
├── README.md
└── release_notes.json
├── MusadoraKitIcon.png
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── MusadoraKit
│ ├── 100 Best Albums
│ ├── HundredBestAlbum.swift
│ ├── HundredBestAlbumRequest.swift
│ └── HundredBestAlbums.swift
│ ├── Add Resources
│ ├── CreatePlaylist.swift
│ ├── LibraryPlaylistCreationRequest.swift
│ └── MAddResourcesRequest.swift
│ ├── Catalog
│ ├── CatalogAlbum.swift
│ ├── CatalogArtist.swift
│ ├── CatalogChart.swift
│ ├── CatalogChart
│ │ ├── ChartItemCollection.swift
│ │ ├── MChartItem.swift
│ │ ├── MChartRequest.swift
│ │ ├── MChartResponse.swift
│ │ └── MCharts.swift
│ ├── CatalogCurator.swift
│ ├── CatalogGenre.swift
│ ├── CatalogMusicVideo.swift
│ ├── CatalogPlaylist.swift
│ ├── CatalogRadioShow.swift
│ ├── CatalogRecordLabel.swift
│ ├── CatalogSearch.swift
│ ├── CatalogSong.swift
│ ├── CatalogStation.swift
│ ├── CatalogSuggestions
│ │ ├── MCatalogSuggestionsRequest.swift
│ │ ├── MCatalogSuggestionsResponse.swift
│ │ ├── MusicCatalogSearchable.swift
│ │ ├── SearchSuggestionItem.swift
│ │ ├── SuggestionKind.swift
│ │ ├── Suggestions.swift
│ │ ├── SuggestionsKind.swift
│ │ ├── TermSuggestion.swift
│ │ └── TopResultsSuggestion.swift
│ ├── MCatalog.swift
│ └── MCatalogSearchType.swift
│ ├── Documentation.docc
│ ├── MusadoraKit.md
│ ├── Tutorials
│ │ ├── Building-Music-Player.tutorial
│ │ ├── Getting-Started.tutorial
│ │ ├── Search-and-Discovery.tutorial
│ │ ├── Working-with-Library.tutorial
│ │ └── table-of-contents.tutorial
│ └── icon.png
│ ├── Equivalents
│ ├── CatalogCleanEquivalent.swift
│ ├── CatalogCleanEquivalents.swift
│ ├── CatalogEquivalent.swift
│ ├── CatalogEquivalents.swift
│ ├── EquivalentMusicItemType.swift
│ └── EquivalentRequestable.swift
│ ├── Extension
│ ├── Array.swift
│ ├── Artwork.swift
│ ├── CommonImageProcessing.swift
│ ├── Data.swift
│ ├── NSColor.swift
│ ├── NSImage.swift
│ ├── UIColor.swift
│ └── UIImage.swift
│ ├── Favorites
│ ├── Favorites.swift
│ └── MFavoritesRequest.swift
│ ├── History
│ ├── MHistory.swift
│ ├── MHistoryEndpoints.swift
│ ├── MHistoryRequest.swift
│ ├── MHistoryResponse.swift
│ ├── MusicRecentlyAddedRequest.swift
│ └── RecentlyPlayed.swift
│ ├── Library
│ ├── LibraryAlbum.swift
│ ├── LibraryArtist.swift
│ ├── LibraryCatalog.swift
│ ├── LibraryGenre.swift
│ ├── LibraryPlaylist.swift
│ ├── LibrarySearch.swift
│ ├── LibrarySong.swift
│ ├── MLibrary.swift
│ ├── MLibraryPlaylist.swift
│ ├── MusicLibrarySearchable.swift
│ ├── Playlist Folder Request
│ │ ├── LibraryPlaylistFolder.swift
│ │ └── MLibraryPlaylistFolder.swift
│ ├── Resource Request
│ │ ├── FilterableLibraryItem.swift
│ │ ├── LibraryMusicItemType.swift
│ │ ├── MLibraryResourceRequest.swift
│ │ └── MLibraryResourceResponse.swift
│ └── Search Request
│ │ ├── MLibrarySearchRequest.swift
│ │ ├── MusadoraLibrarySearchResponse.swift
│ │ ├── MusicLibrarySearchResponseResults.swift
│ │ └── MusicLibrarySearchType.swift
│ ├── Models
│ ├── AppleMusicURLComponents.swift
│ ├── MusadoraKitError.swift
│ ├── RelationshipItem.swift
│ ├── SongsForAlbums.swift
│ ├── SongsForArtists.swift
│ ├── SongsForGenres.swift
│ ├── SongsForPlaylists.swift
│ ├── StationGenre.swift
│ └── UserMusicItem.swift
│ ├── Multiple Resources
│ ├── MusicCatalogResources
│ │ ├── MusicCatalogResourcesRequest.swift
│ │ ├── MusicCatalogResourcesResponse.swift
│ │ └── MusicCatalogResourcesType.swift
│ └── MusicLibraryResources
│ │ ├── MusicLibraryResourcesRequest.swift
│ │ ├── MusicLibraryResourcesResponse.swift
│ │ └── MusicLibraryResourcesType.swift
│ ├── MusadoraKit.swift
│ ├── Music Item Properties
│ ├── AlbumProperties.swift
│ ├── ArtistProperties.swift
│ ├── CuratorProperties.swift
│ ├── MusicVideoProperties.swift
│ ├── PlaylistProperties.swift
│ ├── RadioShowProperties.swift
│ ├── RecordLabelProperties.swift
│ └── SongProperties.swift
│ ├── Music Items
│ ├── Albums.swift
│ ├── Artists.swift
│ ├── Curators.swift
│ ├── Genres.swift
│ ├── LibraryPlaylists.swift
│ ├── MusicVideos.swift
│ ├── Playlists.swift
│ ├── RadioShows.swift
│ ├── RecordLabels.swift
│ ├── Songs.swift
│ ├── StationGenres.swift
│ ├── Stations.swift
│ ├── Tracks.swift
│ └── UserMusicItems.swift
│ ├── Music Player
│ ├── APlayer.swift
│ └── SPlayer.swift
│ ├── Music Summaries
│ ├── MSummary.swift
│ ├── MSummaryRequest.swift
│ └── MSummaryResponse.swift
│ ├── MusicRequest.swift
│ ├── PrivacyInfo.xcprivacy
│ ├── Ratings
│ ├── Catalog
│ │ ├── AddCatalogRating.swift
│ │ ├── CatalogRatingMusicItemType.swift
│ │ ├── DeleteCatalogRating.swift
│ │ ├── GetCatalogRating.swift
│ │ ├── MCatalogRatingAddRequest.swift
│ │ ├── MCatalogRatingDeleteRequest.swift
│ │ └── MCatalogRatingRequest.swift
│ ├── Library
│ │ ├── AddLibraryRating.swift
│ │ ├── DeleteLibraryRating.swift
│ │ ├── GetLibraryRating.swift
│ │ ├── LibraryRatingMusicItemType.swift
│ │ ├── MLibraryRatingAddRequest.swift
│ │ ├── MLibraryRatingDeleteRequest.swift
│ │ └── MLibraryRatingRequest.swift
│ └── Models
│ │ ├── RatingRequest.swift
│ │ ├── RatingType.swift
│ │ ├── Ratings.swift
│ │ └── RatingsResponse.swift
│ ├── Recommendations
│ ├── MRecommendation+default.swift
│ ├── MRecommendation+personal.swift
│ ├── MRecommendation.swift
│ ├── MRecommendationItem.swift
│ ├── MRecommendationMusicItem.swift
│ ├── MRecommendationRequest.swift
│ ├── MRecommendationResponse.swift
│ ├── MRecommendations.swift
│ └── PersonalRecommendations.swift
│ ├── Requests
│ ├── MDataDeleteRequest.swift
│ ├── MDataPostRequest.swift
│ ├── MDataPutRequest.swift
│ ├── MDataRequest.swift
│ ├── MDeveloperTokenProvider.swift
│ └── MUserRequest.swift
│ ├── Storefronts.swift
│ └── Views
│ └── AnimatedArtworkView.swift
├── Tests
└── MusadoraKitTests
│ ├── 100 Best Albums
│ ├── HundredBestAlbumIntegrationTests.swift
│ └── HundredBestAlbumRequestTests.swift
│ ├── Add Resources
│ └── MusicAddResourcesRequestEndpointTests.swift
│ ├── AppleMusicURLComponentsTests.swift
│ ├── Catalog
│ ├── CatalogChartTests.swift
│ ├── CatalogGenreEndpointTests.swift
│ ├── CatalogPlaylistEndpointTests.swift
│ ├── CatalogSearchTests.swift
│ ├── CatalogStationEndpointTests.swift
│ ├── CatalogStationGenreTests.swift
│ ├── CatalogStorefrontTests.swift
│ └── CatalogSuggestionsTests.swift
│ ├── Equivalents
│ └── EquivalentItemsEndpointTests.swift
│ ├── Library
│ └── MLibrarySearchRequestEndpointTests.swift
│ ├── LibraryPlaylistTests.swift
│ ├── MusadoraKitTests.swift
│ ├── Music Items
│ ├── Album.swift
│ ├── Song.swift
│ └── Station.swift
│ ├── MusicHistoryRequestEndpointTests.swift
│ ├── MusicItemPropertiesTests.swift
│ ├── Ratings
│ ├── MusicCatalogRatingRequestTests.swift
│ └── MusicLibraryRatingRequestTests.swift
│ ├── Recommendation
│ └── MusicRecommendationRequestEndpointTests.swift
│ └── Summaries
│ └── MSummaryRequestEndpointTests.swift
└── codemagic.yaml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: rryam
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy DocC Documentation
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 |
13 | concurrency:
14 | group: "pages"
15 | cancel-in-progress: false
16 |
17 | jobs:
18 | build:
19 | runs-on: macos-latest
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v4
23 |
24 | - name: Setup Xcode
25 | uses: maxim-lobanov/setup-xcode@v1
26 | with:
27 | xcode-version: latest-stable
28 |
29 | - name: Build DocC Documentation
30 | run: |
31 | swift package --allow-writing-to-directory ./docs \
32 | generate-documentation --target MusadoraKit \
33 | --output-path ./docs \
34 | --transform-for-static-hosting \
35 | --hosting-base-path MusadoraKit
36 |
37 | - name: Setup Pages
38 | uses: actions/configure-pages@v4
39 |
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v3
42 | with:
43 | path: './docs'
44 |
45 | deploy:
46 | environment:
47 | name: github-pages
48 | url: ${{ steps.deployment.outputs.page_url }}
49 | runs-on: ubuntu-latest
50 | needs: build
51 | steps:
52 | - name: Deploy to GitHub Pages
53 | id: deployment
54 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # macOS
86 | .DS_Store
87 |
88 | # Build logs
89 | *.log
90 |
91 | # Code Injection
92 | #
93 | # After new code Injection tools there's a generated folder /iOSInjectionProject
94 | # https://github.com/johnno1962/injectionforxcode
95 |
96 | iOSInjectionProject/
97 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | external_links:
3 | documentation: "https://rryam.github.io/MusadoraKit/documentation/musadorakit/"
4 |
--------------------------------------------------------------------------------
/.swiftpm/configuration/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-docc-plugin",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-docc-plugin",
7 | "state" : {
8 | "revision" : "2eb22993b3dfd0c0d32729b357c8dabb6cd44680",
9 | "version" : "1.4.2"
10 | }
11 | },
12 | {
13 | "identity" : "swift-docc-symbolkit",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit",
16 | "state" : {
17 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
18 | "version" : "1.0.0"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/MusadoraKitTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
17 |
19 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
46 |
47 |
49 |
50 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Rudrank Riyam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Musadora/Musadora.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Musadora/Musadora.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Musadora/Musadora.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "cd6af0cf2f8ff347cfff7aa71beb2683ef2df0f08d88311dff4cdc7f35af4d20",
3 | "pins" : [
4 | {
5 | "identity" : "musadorakit",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/rryam/MusadoraKit",
8 | "state" : {
9 | "branch" : "main",
10 | "revision" : "a2392987b2b888f040682cebfa0fbae82fe76934"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Group 121.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Assets.xcassets/AppIcon.appiconset/Group 121.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rryam/MusadoraKit/28ae04c064cea01cae18f18d470e230325b8fbe4/Musadora/Musadora/Assets.xcassets/AppIcon.appiconset/Group 121.png
--------------------------------------------------------------------------------
/Musadora/Musadora/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Catalog/CatalogView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 17/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct CatalogView: View {
12 | var body: some View {
13 | NavigationListStack("Catalog") {
14 | NavigationLink("Station Genres", destination: StationGenresView())
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Catalog/Station Genres/StationGenreDetailedView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationGenreDetailedView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 12/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 |
12 | struct StationGenreDetailedView: View {
13 | var stationGenre: StationGenre
14 |
15 | @State private var stations: Stations = []
16 |
17 | var body: some View {
18 | List {
19 | ForEach(stations) { station in
20 | StationRow(station: station)
21 | }
22 | .navigationTitle(stationGenre.name)
23 | }
24 | .task {
25 | do {
26 | stations = try await MCatalog.stations(for: stationGenre)
27 | } catch {
28 | print(error)
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Catalog/Station Genres/StationGenresView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationGenresView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 12/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct StationGenresView: View {
12 | @State private var stationGenres: StationGenres = []
13 |
14 | var body: some View {
15 | List {
16 | ForEach(stationGenres) { stationGenre in
17 | NavigationLink(stationGenre.name, destination: StationGenreDetailedView(stationGenre: stationGenre))
18 | }
19 | .navigationTitle("Station Genres")
20 | }
21 | .task {
22 | await fetchStationGenres()
23 | }
24 | }
25 | }
26 |
27 | extension StationGenresView {
28 | private func fetchStationGenres() async {
29 | do {
30 | stationGenres = try await MCatalog.stationGenres()
31 | } catch {
32 | print(error)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Charts/AlbumChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumChartView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | import MusadoraKit
11 |
12 | struct AlbumChartView: View {
13 | var albumChart: MusicCatalogChart
14 |
15 | var body: some View {
16 | List {
17 | ForEach(albumChart.items) { album in
18 | VStack(alignment: .leading) {
19 | HStack {
20 | if let artwork = album.artwork {
21 | ArtworkImage(artwork, width: 100, height: 100)
22 | .cornerRadius(8)
23 | }
24 |
25 | Spacer()
26 |
27 | Image(systemName: "play.fill")
28 | .foregroundColor(.secondary)
29 | .onTapGesture {
30 | Task {
31 | do {
32 | try await APlayer.shared.play(album: album)
33 | } catch {
34 | print(error)
35 | }
36 | }
37 | }
38 | }
39 |
40 | Text(album.title)
41 | .bold()
42 | .font(.headline)
43 |
44 | Text(album.artistName)
45 | .font(.subheadline)
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Charts/ChartsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct ChartsView: View {
12 | @State private var genres: Genres = []
13 |
14 | #if os(iOS)
15 | private let columns = [GridItem(.flexible()), GridItem(.flexible())]
16 | #else
17 | private let columns = [GridItem(.adaptive(minimum: 220))]
18 | #endif
19 |
20 | var body: some View {
21 | NavigationStack {
22 | ScrollView {
23 | LazyVGrid(columns: columns, spacing: 12) {
24 | ForEach(genres) { genre in
25 | NavigationLink(destination: { ChartView(genre: genre) }, label: {
26 | Text(genre.name)
27 | .font(.headline)
28 | .multilineTextAlignment(.center)
29 | .frame(maxWidth: .infinity, minHeight: 80)
30 | .padding(12)
31 | .background(
32 | RoundedRectangle(cornerRadius: 12)
33 | .fill(Color.gray.opacity(0.1))
34 | )
35 | .overlay(
36 | RoundedRectangle(cornerRadius: 12)
37 | .stroke(Color.secondary.opacity(0.2))
38 | )
39 | .contentShape(RoundedRectangle(cornerRadius: 12))
40 | })
41 | .buttonStyle(.plain)
42 | }
43 | }
44 | .padding(.horizontal)
45 | .padding(.top)
46 | }
47 | .navigationTitle("Charts")
48 | }
49 | .task {
50 | do {
51 | genres = try await MCatalog.topGenres()
52 | } catch {
53 | print(error)
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Charts/MusicVideoChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicVideoChartView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | import SwiftUI
11 |
12 | import MusadoraKit
13 |
14 | struct MusicVideoChartView: View {
15 | var musicVideoChart: MusicCatalogChart
16 |
17 | var body: some View {
18 | List {
19 | ForEach(musicVideoChart.items) { musicVideo in
20 | VStack(alignment: .leading) {
21 | if let artwork = musicVideo.artwork {
22 | ArtworkImage(artwork, width: 80, height: 80)
23 | .cornerRadius(8)
24 | }
25 |
26 | Text(musicVideo.title)
27 | .bold()
28 | .font(.headline)
29 |
30 | Text(musicVideo.artistName)
31 | .font(.subheadline)
32 | }
33 | }
34 | }
35 | .navigationTitle(musicVideoChart.title)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Charts/PlaylistChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaylistChartView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | import MusadoraKit
11 |
12 | struct PlaylistChartView: View {
13 | var playlistChart: MusicCatalogChart
14 |
15 | var body: some View {
16 | List {
17 | ForEach(playlistChart.items) { playlist in
18 | VStack(alignment: .leading) {
19 | HStack {
20 | if let artwork = playlist.artwork {
21 | ArtworkImage(artwork, width: 100, height: 100)
22 | .cornerRadius(8)
23 | }
24 |
25 | Spacer()
26 |
27 | Button(action: {
28 | Task {
29 | do {
30 | try await APlayer.shared.play(playlist: playlist)
31 | } catch {
32 | print(error)
33 | }
34 | }
35 | }, label: {
36 | Image(systemName: "play.fill")
37 | })
38 | .buttonStyle(.plain)
39 | }
40 |
41 | Text(playlist.name)
42 | .bold()
43 | .font(.headline)
44 |
45 | Text(playlist.curatorName ?? "")
46 | .font(.subheadline)
47 | }
48 | }
49 | }
50 | .navigationTitle(playlistChart.title)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Charts/SongChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongChartView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | import MusadoraKit
11 |
12 | struct SongChartView: View {
13 | var songChart: MusicCatalogChart
14 |
15 | var body: some View {
16 | SongsView(with: songChart.items)
17 | .navigationTitle(songChart.title)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Musadora/Musadora/History/MusicSummariesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicSummariesView.swift
3 | // Musadora
4 | //
5 | // Created by Codex on 02/09/25.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 | import MusicKit
11 |
12 | struct MusicSummariesView: View {
13 | @State private var topArtists: Artists = []
14 | @State private var topAlbums: Albums = []
15 | @State private var topSongs: Songs = []
16 | @State private var year: Int?
17 | @State private var errorMessage: String?
18 | @State private var isEligible: Bool = false
19 |
20 | var body: some View {
21 | List {
22 | if let message = errorMessage {
23 | Section {
24 | Text(message)
25 | .foregroundStyle(.secondary)
26 | }
27 | }
28 |
29 | if isEligible {
30 | if let year {
31 | Text("Year: \(year)")
32 | .font(.headline)
33 | }
34 |
35 | NavigationLink("Top Songs", destination: SongsView(with: topSongs))
36 | NavigationLink("Top Albums", destination: AlbumsView(with: topAlbums))
37 |
38 | Section("Top Artists") {
39 | ForEach(topArtists) { artist in
40 | Text(artist.name)
41 | }
42 | }
43 | }
44 | }
45 | .navigationTitle("Music Summaries")
46 | .task(id: MusicAuthorization.currentStatus) {
47 | await checkEligibilityAndLoad()
48 | }
49 | }
50 | }
51 |
52 | extension MusicSummariesView {
53 | @MainActor
54 | private func checkEligibilityAndLoad() async {
55 | errorMessage = nil
56 | isEligible = false
57 |
58 | // Do not start request until authorized
59 | let status = MusicAuthorization.currentStatus
60 | guard status == .authorized else {
61 | errorMessage = "Not authorized for Apple Music. Tap Continue on the welcome screen."
62 | return
63 | }
64 |
65 | // Ensure user has an active Apple Music subscription
66 | let subscription: MusicSubscription? = try? await MusicSubscription.current
67 | guard subscription?.canPlayCatalogContent == true else {
68 | errorMessage = "Requires an active Apple Music subscription to load Replay."
69 | return
70 | }
71 |
72 | do {
73 | let summary = try await MSummary.latest()
74 | self.topArtists = summary.topArtists
75 | self.topAlbums = summary.topAlbums
76 | self.topSongs = summary.topSongs
77 | self.year = summary.year
78 | self.isEligible = true
79 | } catch is CancellationError {
80 | // Task was cancelled by SwiftUI lifecycle; ignore
81 | } catch {
82 | print("DEBUG: Full error details: \(error)")
83 | errorMessage = "Could not load Replay (\(error.localizedDescription))."
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Musadora/Musadora/History/RecentlyAddedView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentlyAddedView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct RecentlyAddedView: View {
12 | @State private var recentlyAddedPlaylists: Playlists = []
13 | @State private var recentlyAddedAlbums: Albums = []
14 |
15 | var body: some View {
16 | List {
17 | NavigationLink("Playlists", destination: PlaylistsView(with: recentlyAddedPlaylists))
18 | NavigationLink("Albums", destination: AlbumsView(with: recentlyAddedAlbums))
19 | }
20 | .navigationTitle("Recently Added")
21 | .task {
22 | do {
23 | recentlyAddedPlaylists = try await MHistory.recentlyAddedPlaylists(limit: 25, offset: 0)
24 | recentlyAddedAlbums = try await MHistory.recentlyAddedAlbums(limit: 25, offset: 0)
25 | } catch {
26 | print(error)
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Musadora/Musadora/History/RecentlyPlayedView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentlyPlayedView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct RecentlyPlayedView: View {
12 | @State private var recentlyPlayedPlaylists: Playlists = []
13 | @State private var recentlyPlayedAlbums: Albums = []
14 |
15 | var body: some View {
16 | List {
17 | NavigationLink("Playlists", destination: PlaylistsView(with: recentlyPlayedPlaylists))
18 | NavigationLink("Albums", destination: AlbumsView(with: recentlyPlayedAlbums))
19 | }
20 | .navigationTitle("Recently Added")
21 | .task {
22 | do {
23 | recentlyPlayedAlbums = try await MHistory.recentlyPlayedAlbums(limit: 25, offset: 0)
24 | recentlyPlayedPlaylists = try await MHistory.recentlyPlayedPlaylists(limit: 25, offset: 0)
25 | } catch {
26 | print(error)
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ITSAppUsesNonExemptEncryption
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Library/LibraryAlbumsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryAlbumsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct LibraryAlbumsView: View {
12 | @State private var albums: Albums = []
13 |
14 | var body: some View {
15 | AlbumsView(with: albums)
16 | .navigationTitle("Albums")
17 | .task {
18 | do {
19 | albums = try await MLibrary.albums()
20 | } catch {
21 | print(error)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Library/LibraryPlaylistsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryPlaylistsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct LibraryPlaylistsView: View {
12 | @State private var playlists: Playlists = []
13 |
14 | var body: some View {
15 | PlaylistsView(with: playlists)
16 | .navigationTitle("Playlists")
17 | .task {
18 | do {
19 | playlists = try await MLibrary.playlists()
20 | } catch {
21 | print(error)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Library/LibrarySongsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibrarySongsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct LibrarySongsView: View {
12 | @State private var songs: Songs = []
13 |
14 | var body: some View {
15 | SongsView(with: songs)
16 | .navigationTitle("Songs")
17 | .task {
18 | do {
19 | songs = try await MLibrary.songs()
20 | } catch {
21 | print(error)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Library/LibraryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 09/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct LibraryView: View {
12 | var body: some View {
13 | NavigationStack {
14 | List {
15 | Section("Library") {
16 | NavigationLink("Songs") { LibrarySongsView() }
17 | NavigationLink("Albums") { LibraryAlbumsView() }
18 | NavigationLink("Playlists") { LibraryPlaylistsView() }
19 | }
20 |
21 | Section("History") {
22 | NavigationLink("Recently Added") { RecentlyAddedView() }
23 | NavigationLink("Recently Played") { RecentlyPlayedView() }
24 | NavigationLink("Music Summaries") { MusicSummariesView() }
25 | }
26 | }
27 | .navigationTitle("Library")
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Main/MusadoraApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusadoraApp.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 09/03/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct MusadoraApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | MTabView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Album/AlbumRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumRow.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct AlbumRow: View {
12 | var album: Album
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | HStack {
17 | if let artwork = album.artwork {
18 | ArtworkImage(artwork, width: 100, height: 100)
19 | .cornerRadius(8)
20 | }
21 |
22 | Spacer()
23 |
24 | Image(systemName: "play.fill")
25 | .foregroundColor(.secondary)
26 | .onTapGesture {
27 | Task {
28 | do {
29 | try await APlayer.shared.play(album: album)
30 | } catch {
31 | print(error)
32 | }
33 | }
34 | }
35 | }
36 |
37 | Text(album.title)
38 | .bold()
39 | .font(.headline)
40 |
41 | Text(album.artistName)
42 | .font(.subheadline)
43 | }
44 | .contentShape(Rectangle())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Album/AlbumsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct AlbumsView: View {
12 | private var albums: Albums
13 |
14 | init(with albums: Albums) {
15 | self.albums = albums
16 | }
17 |
18 | var body: some View {
19 | ScrollView {
20 | LazyVStack(spacing: 20) {
21 | if albums.count > 4 {
22 | // Show top 4 in grid
23 | VStack(alignment: .leading, spacing: 16) {
24 | Text("Top Albums")
25 | .font(.title2)
26 | .fontWeight(.bold)
27 | .padding(.horizontal)
28 |
29 | LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
30 | ForEach(albums.prefix(4), id: \.id) { album in
31 | AlbumGridCard(album: album)
32 | }
33 | }
34 | .padding(.horizontal)
35 | }
36 |
37 | // Show remaining albums in list
38 | if albums.count > 4 {
39 | VStack(alignment: .leading, spacing: 12) {
40 | Text("All Albums")
41 | .font(.title3)
42 | .fontWeight(.semibold)
43 | .padding(.horizontal)
44 | .padding(.top)
45 |
46 | LazyVStack(spacing: 8) {
47 | ForEach(albums, content: AlbumRow.init)
48 | }
49 | .padding(.horizontal)
50 | }
51 | }
52 | } else {
53 | // Show all albums in grid if 4 or fewer
54 | LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
55 | ForEach(albums, id: \.id) { album in
56 | AlbumGridCard(album: album)
57 | }
58 | }
59 | .padding(.horizontal)
60 | }
61 | }
62 | }
63 | .navigationTitle("Top Albums")
64 | .navigationBarTitleDisplayMode(.large)
65 | }
66 | }
67 |
68 | struct AlbumGridCard: View {
69 | let album: Album
70 |
71 | var body: some View {
72 | VStack(alignment: .leading, spacing: 8) {
73 | if let artwork = album.artwork {
74 | ArtworkImage(artwork, width: 150, height: 150)
75 | .cornerRadius(12)
76 | }
77 |
78 | VStack(alignment: .leading, spacing: 4) {
79 | Text(album.title)
80 | .font(.headline)
81 | .lineLimit(2)
82 |
83 | Text(album.artistName)
84 | .font(.subheadline)
85 | .foregroundColor(.secondary)
86 | .lineLimit(1)
87 | }
88 | }
89 | .onTapGesture {
90 | Task {
91 | do {
92 | try await APlayer.shared.play(album: album)
93 | } catch {
94 | print(error)
95 | }
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Playlist/PlaylistRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaylistRow.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct PlaylistRow: View {
12 | var playlist: Playlist
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | HStack {
17 | if let artwork = playlist.artwork {
18 | ArtworkImage(artwork, width: 100, height: 100)
19 | .cornerRadius(8)
20 | }
21 |
22 | Spacer()
23 |
24 | Image(systemName: "play.fill")
25 | .foregroundColor(.secondary)
26 | .onTapGesture {
27 | Task {
28 | do {
29 | try await APlayer.shared.play(playlist: playlist)
30 | } catch {
31 | print(error)
32 | }
33 | }
34 | }
35 | }
36 |
37 | Text(playlist.name)
38 | .bold()
39 | .font(.headline)
40 |
41 | Text(playlist.curatorName ?? "")
42 | .font(.subheadline)
43 | }
44 | .contentShape(Rectangle())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Playlist/PlaylistsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaylistsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct PlaylistsView: View {
12 | private var playlists: Playlists
13 |
14 | init(with playlists: Playlists) {
15 | self.playlists = playlists
16 | }
17 |
18 | var body: some View {
19 | List {
20 | ForEach(playlists, content: PlaylistRow.init)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Song/SongRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongRow.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct SongRow: View {
12 | var song: Song
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | HStack {
17 | if let artwork = song.artwork {
18 | ArtworkImage(artwork, width: 100, height: 100)
19 | .cornerRadius(8)
20 | }
21 |
22 | Spacer()
23 |
24 | Image(systemName: "play.fill")
25 | .foregroundColor(.secondary)
26 | .onTapGesture {
27 | Task {
28 | do {
29 | try await APlayer.shared.play(song: song)
30 | } catch {
31 | print(error)
32 | }
33 | }
34 | }
35 | }
36 |
37 | Text(song.title)
38 | .bold()
39 | .font(.headline)
40 |
41 | Text(song.artistName)
42 | .font(.subheadline)
43 | }
44 | .contentShape(Rectangle())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Song/SongsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongsView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct SongsView: View {
12 | private var songs: Songs
13 |
14 | init(with songs: Songs) {
15 | self.songs = songs
16 | }
17 |
18 | var body: some View {
19 | List {
20 | ForEach(songs.enumerated().map({ $0 }), id: \.element.id) { index, song in
21 | HStack(spacing: 12) {
22 | // Ranking number
23 | Text("\(index + 1)")
24 | .font(.headline)
25 | .fontWeight(.bold)
26 | .foregroundColor(.primary)
27 | .frame(minWidth: 24)
28 |
29 | // Song artwork
30 | if let artwork = song.artwork {
31 | ArtworkImage(artwork, width: 50, height: 50)
32 | .cornerRadius(8)
33 | }
34 |
35 | // Song details
36 | VStack(alignment: .leading, spacing: 2) {
37 | Text(song.title)
38 | .font(.headline)
39 | .lineLimit(1)
40 |
41 | Text(song.artistName)
42 | .font(.subheadline)
43 | .foregroundColor(.secondary)
44 | .lineLimit(1)
45 | }
46 |
47 | Spacer()
48 |
49 | // Play button
50 | Button(action: {
51 | Task {
52 | do {
53 | try await APlayer.shared.play(song: song)
54 | } catch {
55 | print(error)
56 | }
57 | }
58 | }) {
59 | Image(systemName: "play.fill")
60 | .foregroundColor(.primary)
61 | }
62 | .buttonStyle(PlainButtonStyle())
63 | }
64 | .contentShape(Rectangle())
65 | .onTapGesture {
66 | Task {
67 | do {
68 | try await APlayer.shared.play(song: song)
69 | } catch {
70 | print(error)
71 | }
72 | }
73 | }
74 | }
75 | }
76 | .navigationTitle("Top Songs")
77 | .navigationBarTitleDisplayMode(.large)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Music Items/Station/StationRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationRow.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct StationRow: View {
12 | var station: Station
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | HStack {
17 | if let artwork = station.artwork {
18 | ArtworkImage(artwork, width: 100, height: 100)
19 | .cornerRadius(8)
20 | }
21 |
22 | Spacer()
23 |
24 | Image(systemName: "play.fill")
25 | .foregroundColor(.secondary)
26 | .onTapGesture {
27 | Task {
28 | do {
29 | try await APlayer.shared.play(station: station)
30 | } catch {
31 | print(error)
32 | }
33 | }
34 | }
35 | }
36 |
37 | Text(station.name)
38 | .bold()
39 | .font(.headline)
40 |
41 | Text("\(station.editorialNotes?.short ?? "") • \(station.editorialNotes?.standard ?? "")")
42 | .font(.subheadline)
43 | }
44 | .contentShape(Rectangle())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Musadora/Musadora/README.md:
--------------------------------------------------------------------------------
1 | # Musadora
2 |
3 | Musadora is a sample project to show all the methods and powers of MusadoraKit. It is work in progress right now.
4 |
5 | ## Installation
6 |
7 | You can change the bundle identifier to something of your own that has MusicKit services enabled. Then, build the project on your phone.
8 |
9 | ## Playing Music
10 |
11 | Just long press on music items to start playing it.
12 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Recommendations/RecommendationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecommendationView.swift
3 | // Musadora
4 | //
5 | // Created by Rudrank Riyam on 14/03/23.
6 | //
7 |
8 | import SwiftUI
9 | import MusadoraKit
10 |
11 | struct RecommendationView: View {
12 | var recommendation: MRecommendationItem
13 |
14 | var body: some View {
15 | ScrollView(.horizontal) {
16 | LazyHStack {
17 | ForEach(recommendation.items, content: RecommendationRow.init)
18 | }
19 | }
20 | }
21 | }
22 |
23 | struct RecommendationRow: View {
24 | var item: UserMusicItem
25 |
26 | var body: some View {
27 | Button(action: {
28 | Task {
29 | do {
30 | APlayer.shared.queue = [item]
31 | try await APlayer.shared.play()
32 | } catch {
33 | print(error)
34 | }
35 | }
36 | }, label: {
37 | VStack(alignment: .leading) {
38 | if let artwork = item.artwork {
39 | ArtworkImage(artwork, width: 250, height: 250)
40 | .cornerRadius(16)
41 | } else {
42 | Color.red
43 | }
44 |
45 | Text(item.title)
46 | .bold()
47 | .font(.headline)
48 | }
49 | .padding(8)
50 | .contentShape(RoundedRectangle(cornerRadius: 24))
51 | .hoverEffect()
52 | })
53 | .buttonStyle(.plain)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Musadora/Musadora/Shared/NavigationListStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationListStack.swift
3 | // Musadora
4 | //
5 | // Extracted to a shared location to be reused across views.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NavigationListStack: View where Content: View {
11 | var title: String
12 | var content: () -> Content
13 |
14 | init(_ title: String, @ViewBuilder content: @escaping () -> Content) {
15 | self.title = title
16 | self.content = content
17 | }
18 |
19 | var body: some View {
20 | NavigationStack {
21 | ScrollView {
22 | content()
23 | }
24 | .navigationTitle(title)
25 | }
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Musadora/README.md:
--------------------------------------------------------------------------------
1 | # Musadora
2 |
3 | Musadora is a sample project to show all the methods and powers of MusadoraKit. It is work in progress right now.
4 |
5 | ## Installation
6 |
7 | You can change the bundle identifier to something of your own that has MusicKit services enabled. Then, build the project on your phone.
8 |
9 | ## Playing Music
10 |
11 | Just long press on music items to start playing it.
12 |
--------------------------------------------------------------------------------
/Musadora/release_notes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "language": "en-GB",
4 | "text": "Hey there! Welcome to Musadora app. Updated history to include in library tab. Thanks Jay!"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/MusadoraKitIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rryam/MusadoraKit/28ae04c064cea01cae18f18d470e230325b8fbe4/MusadoraKitIcon.png
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "18d3baf3f486d1c7e213e87377b79507478b24d548fac6da4bbdb647aa06a07f",
3 | "pins" : [
4 | {
5 | "identity" : "swift-docc-plugin",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/apple/swift-docc-plugin",
8 | "state" : {
9 | "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06",
10 | "version" : "1.4.5"
11 | }
12 | },
13 | {
14 | "identity" : "swift-docc-symbolkit",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit",
17 | "state" : {
18 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
19 | "version" : "1.0.0"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.2
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MusadoraKit",
8 | platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v8), .tvOS(.v15), .visionOS(.v1)],
9 | products: [
10 | .library(name: "MusadoraKit", targets: ["MusadoraKit"])
11 | ],
12 | dependencies: [
13 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0")
14 | ],
15 | targets: [
16 | .target(
17 | name: "MusadoraKit",
18 | dependencies: [],
19 | resources: [
20 | .copy("PrivacyInfo.xcprivacy")
21 | ],
22 | swiftSettings: [
23 | .enableUpcomingFeature("StrictConcurrency")
24 | ]
25 | ),
26 | .testTarget(name: "MusadoraKitTests", dependencies: ["MusadoraKit"])
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogChart/ChartItemCollection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartItemCollection.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A collection of chart items.
11 | public struct ChartItemCollection where MusicItemType: MusicItem {
12 | /// The unique name of the chart to use when fetching a specific chart.
13 | public let chart: String
14 |
15 | /// The localized display name for the chart.
16 | public let name: String
17 |
18 | /// A relative cursor to fetch the next paginated results for the chart if more exist.
19 | public let next: String?
20 |
21 | /// The popularity-ordered music item type for the chart.
22 | public let items: [MusicItemType]
23 |
24 | enum CodingKeys: String, CodingKey {
25 | case chart, name
26 | case next
27 | case items = "data"
28 | }
29 | }
30 |
31 | public extension ChartItemCollection {
32 | /// A Boolean value that indicates whether the collection has information
33 | /// that allows it to fetch a subsequent batch of items.
34 | var hasNextBatch: Bool {
35 | next == nil
36 | }
37 | }
38 |
39 | extension ChartItemCollection where MusicItemType: MChartItem {}
40 |
41 | extension ChartItemCollection: Decodable where MusicItemType: Decodable {}
42 |
43 | extension ChartItemCollection: Encodable where MusicItemType: Encodable {}
44 |
45 | extension ChartItemCollection: Equatable, Hashable {
46 | public static func == (lhs: ChartItemCollection, rhs: ChartItemCollection) -> Bool {
47 | lhs.chart == rhs.chart
48 | }
49 |
50 | public func hash(into hasher: inout Hasher) {
51 | hasher.combine(chart)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogChart/MChartItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MChartItem.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// A protocol for music items that your app can fetch by
9 | /// using a catalog chart request.
10 | public protocol MChartItem {}
11 |
12 | extension MChartItem {
13 | static var objectIdentifier: ObjectIdentifier {
14 | ObjectIdentifier(Self.self)
15 | }
16 | }
17 |
18 | extension Song: MChartItem {}
19 |
20 | extension Playlist: MChartItem {}
21 |
22 | extension MusicVideo: MChartItem {}
23 |
24 | extension Album: MChartItem {}
25 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogChart/MChartRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MChartRequest.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 26/03/22.
6 | //
7 |
8 | import Foundation
9 |
10 | enum MChartType: String {
11 | case cityCharts
12 | case dailyGlobalTopCharts
13 | }
14 |
15 | /// A chart request that your app uses to fetch charts from the Apple Music catalog
16 | /// using the types of charts and for the genre identifier.
17 | struct MChartRequest {
18 | /// The identifier for the genre to use in the chart results.
19 | var genre: MusicItemID?
20 |
21 | /// A limit for the number of items to return
22 | /// in the catalog chart response.
23 | var limit: Int?
24 |
25 | var chartType: [MChartType]?
26 |
27 | /// Creates a request to fetch charts using the list of the
28 | /// types of charts to include in the results.
29 | init(types: [MChartItem.Type]) {
30 | self.types = Set(types.map { $0.objectIdentifier }).compactMap {
31 | switch $0 {
32 | case Song.objectIdentifier:
33 | return "songs"
34 | case Album.objectIdentifier:
35 | return "albums"
36 | case MusicVideo.objectIdentifier:
37 | return "music-videos"
38 | case Playlist.objectIdentifier:
39 | return "playlists"
40 | default:
41 | return nil
42 | }
43 | }.joined(separator: ",")
44 | }
45 |
46 | /// Fetches charts of the requested catalog chart types that match
47 | /// the genre identifier of the request.
48 | func response() async throws -> MChartResponse {
49 | let storefront = try await MusicDataRequest.currentCountryCode
50 | let url = try chartsURL(storefront: storefront)
51 | let request = MusicDataRequest(urlRequest: .init(url: url))
52 | let response = try await request.response()
53 |
54 | let charts = try JSONDecoder().decode(MCharts.self, from: response.data)
55 | return charts.results
56 | }
57 |
58 | private var types: String
59 | }
60 |
61 | extension MChartRequest {
62 | internal func chartsURL(storefront: String) throws -> URL {
63 | var components = AppleMusicURLComponents()
64 | var queryItems: [URLQueryItem] = []
65 | components.path = "catalog/\(storefront)/charts"
66 |
67 | queryItems.append(URLQueryItem(name: "types", value: types))
68 |
69 | if let genre = genre {
70 | queryItems.append(URLQueryItem(name: "genre", value: genre.rawValue))
71 | }
72 |
73 | if let limit = limit {
74 | queryItems.append(URLQueryItem(name: "limit", value: "\(limit)"))
75 | }
76 |
77 | if let chartType = chartType, !chartType.isEmpty {
78 | let value = chartType.map { $0.rawValue }.joined(separator: ",")
79 | queryItems.append(URLQueryItem(name: "with", value: value))
80 | }
81 |
82 | components.queryItems = queryItems
83 |
84 | guard let url = components.url else {
85 | throw URLError(.badURL)
86 | }
87 | return url
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogChart/MCharts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCharts.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The response to a request for a chart.
11 | ///
12 | /// This structure represents the response from the Apple Music API when requesting chart data.
13 | /// It contains the results of the chart request, which can include various types of music items like songs, albums, and playlists.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// let charts = try await MCatalog.charts(kinds: .dailyGlobalTop, types: .songs)
18 | /// print(charts.results.songs)
19 | /// ```
20 | struct MCharts: Codable {
21 | /// A mapping of a requested type to an array of charts.
22 | let results: MChartResponse
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogRadioShow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogRadioShow.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 10/04/22.
6 | //
7 |
8 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
9 | public extension MCatalog {
10 | /// Fetch a radio show from the Apple Music catalog by using its identifier.
11 | /// - Parameters:
12 | /// - id: The unique identifier for the radio show.
13 | /// - properties: Additional relationships to fetch with the radio show.
14 | /// - Returns: `RadioShow` matching the given identifier.
15 | static func radioShow(id: MusicItemID, fetch properties: RadioShowProperties) async throws -> RadioShow {
16 | var request = MusicCatalogResourceRequest(matching: \.id, equalTo: id)
17 | request.properties = properties
18 | let response = try await request.response()
19 |
20 | guard let radioShow = response.items.first else {
21 | throw MusadoraKitError.notFound(for: id.rawValue)
22 | }
23 | return radioShow
24 | }
25 |
26 | /// Fetch a radio show from the Apple Music catalog by using its identifier with all properties.
27 | /// - Parameters:
28 | /// - id: The unique identifier for the radio show.
29 | /// - Returns: `RadioShow` matching the given identifier.
30 | static func radioShow(id: MusicItemID) async throws -> RadioShow {
31 | var request = MusicCatalogResourceRequest(matching: \.id, equalTo: id)
32 | request.properties = .all
33 | let response = try await request.response()
34 |
35 | guard let radioShow = response.items.first else {
36 | throw MusadoraKitError.notFound(for: id.rawValue)
37 | }
38 | return radioShow
39 | }
40 |
41 | /// Fetch multiple radio shows from the Apple Music catalog by using their identifiers.
42 | /// - Parameters:
43 | /// - ids: The unique identifiers for the radio shows.
44 | /// - properties: Additional relationships to fetch with the radio shows.
45 | /// - Returns: `RadioShows` matching the given identifiers.
46 | static func radioShows(ids: [MusicItemID], fetch properties: RadioShowProperties) async throws -> RadioShows {
47 | var request = MusicCatalogResourceRequest(matching: \.id, memberOf: ids)
48 | request.properties = properties
49 | let response = try await request.response()
50 | return response.items
51 | }
52 |
53 | /// Fetch multiple radio shows from the Apple Music catalog by using their identifiers.
54 | /// - Parameters:
55 | /// - ids: The unique identifiers for the radio shows.
56 | /// - Returns: `RadioShows` matching the given identifiers.
57 | static func radioShows(ids: [MusicItemID]) async throws -> RadioShows {
58 | var request = MusicCatalogResourceRequest(matching: \.id, memberOf: ids)
59 | request.properties = .all
60 | let response = try await request.response()
61 | return response.items
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/MCatalogSuggestionsResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCatalogSuggestionsResponse.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 09/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// An object that contains results for a catalog suggestions request.
11 | ///
12 | /// This structure provides access to search suggestions and top results based on
13 | /// a user's search term, helping users discover content more efficiently.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// let request = MCatalogSuggestionsRequest(term: "taylor")
18 | /// let response = try await request.response()
19 | ///
20 | /// // Access search term suggestions
21 | /// for term in response.terms {
22 | /// print(term.displayTerm)
23 | /// }
24 | ///
25 | /// // Access top results
26 | /// for result in response.topResults {
27 | /// print(result.title)
28 | /// }
29 | /// ```
30 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
31 | struct MCatalogSuggestionsResponse {
32 | /// A collection of search and display terms.
33 | var terms: [TermSuggestion] = []
34 |
35 | /// A collection of different top results.
36 | var topResults: [TopResultsSuggestion] = []
37 | }
38 |
39 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
40 | extension MCatalogSuggestionsResponse: Codable {
41 | private enum CodingKeys: String, CodingKey {
42 | case suggestions
43 | }
44 |
45 | init(from decoder: Decoder) throws {
46 | let container = try decoder.container(keyedBy: CodingKeys.self)
47 | let suggestions = try container.decode([SuggestionKind].self, forKey: .suggestions)
48 |
49 | for suggestion in suggestions {
50 | switch suggestion {
51 | case let .terms(term):
52 | terms.append(term)
53 |
54 | case let .topResults(topResult):
55 | topResults.append(topResult)
56 | }
57 | }
58 | }
59 |
60 | func encode(to _: Encoder) throws {}
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/MusicCatalogSearchable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicCatalogSearchable.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
9 | extension MusicCatalogSearchable {
10 | static var identifier: ObjectIdentifier {
11 | ObjectIdentifier(Self.self)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/SearchSuggestionItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchSuggestionItem.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// The top search suggestion types.
9 | /// Possible types: Albums, RadioShows, Artists, Curators, MusicVideos, Playlists, RecordLabels, Songs, Stations.
10 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
11 | public enum SearchSuggestionItem: Equatable, Hashable {
12 | case album(Album)
13 | case song(Song)
14 | case musicVideo(MusicVideo)
15 | case artist(Artist)
16 | case playlist(Playlist)
17 | case radioShow(RadioShow)
18 | case curator(Curator)
19 | case station(Station)
20 | case recordLabel(RecordLabel)
21 | }
22 |
23 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
24 | extension SearchSuggestionItem: Codable {
25 | enum CodingKeys: CodingKey {
26 | case type
27 | }
28 |
29 | public init(from decoder: Decoder) throws {
30 | let values = try decoder.container(keyedBy: CodingKeys.self)
31 | let type = try values.decode(String.self, forKey: .type)
32 |
33 | switch type {
34 | case "albums":
35 | let album = try Album(from: decoder)
36 | self = .album(album)
37 | case "songs":
38 | let song = try Song(from: decoder)
39 | self = .song(song)
40 | case "stations":
41 | let station = try Station(from: decoder)
42 | self = .station(station)
43 | case "music-videos":
44 | let musicVideo = try MusicVideo(from: decoder)
45 | self = .musicVideo(musicVideo)
46 | case "playlists":
47 | let playlist = try Playlist(from: decoder)
48 | self = .playlist(playlist)
49 | case "record-labels":
50 | let recordLabel = try RecordLabel(from: decoder)
51 | self = .recordLabel(recordLabel)
52 | case "artists":
53 | let artist = try Artist(from: decoder)
54 | self = .artist(artist)
55 | case "apple-curators":
56 | let radioShow = try RadioShow(from: decoder)
57 | self = .radioShow(radioShow)
58 | case "curators":
59 | let curator = try Curator(from: decoder)
60 | self = .curator(curator)
61 | default:
62 | let decodingErrorContext = DecodingError.Context(
63 | codingPath: decoder.codingPath,
64 | debugDescription: "Unexpected type \"\(type)\" encountered for SearchSuggestionItem."
65 | )
66 | throw DecodingError.typeMismatch(SearchSuggestionItem.self, decodingErrorContext)
67 | }
68 | }
69 |
70 | public func encode(to _: Encoder) throws {}
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/SuggestionKind.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SuggestionKind.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// The suggestion kinds to include in the results.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public enum SuggestionKind: Codable {
11 | case terms(TermSuggestion)
12 | case topResults(TopResultsSuggestion)
13 |
14 | private enum CodingKeys: String, CodingKey {
15 | case kind
16 | }
17 |
18 | public init(from decoder: Decoder) throws {
19 | let container = try decoder.container(keyedBy: CodingKeys.self)
20 | let kind = try container.decode(SuggestionsKind.self, forKey: .kind)
21 |
22 | switch kind {
23 | case .terms:
24 | let termSuggestion = try TermSuggestion(from: decoder)
25 | self = .terms(termSuggestion)
26 | case .topResults:
27 | let topResultsSuggestion = try TopResultsSuggestion(from: decoder)
28 | self = .topResults(topResultsSuggestion)
29 | }
30 | }
31 |
32 | public func encode(to _: Encoder) throws {}
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/Suggestions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Suggestions.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The response to a request for search suggestions.
11 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
12 | public struct Suggestions: Codable {
13 | /// The results included in the response to a request for search suggestions.
14 | let results: MCatalogSuggestionsResponse
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/SuggestionsKind.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SuggestionsKind.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// The suggestion kinds to include in the results.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, visionOS 1.0, *)
10 | public enum SuggestionsKind: String, Codable {
11 | case terms
12 | case topResults
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/TermSuggestion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TermSuggestion.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// A suggested search term from a search suggestion response.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public struct TermSuggestion: Codable, Equatable, Hashable {
11 | /// The kind of suggestion.
12 | /// Value: terms
13 | public let kind: SuggestionsKind
14 |
15 | /// The term to use as a search input when using this suggestion.
16 | public let searchTerm: String
17 |
18 | /// A potentially censored term to display to the user to select from. Use the `searchTerm` value for the actual search.
19 | public let displayTerm: String
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/CatalogSuggestions/TopResultsSuggestion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopResultsSuggestion.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A suggested popular result for similar search prefix terms.
11 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
12 | public struct TopResultsSuggestion: Codable, Equatable, Hashable {
13 | /// The kind of suggestion.
14 | /// Value: topResults
15 | public let kind: SuggestionsKind
16 |
17 | /// The actual resource for the suggested content.
18 | /// Possible types: Albums, RadioShows, Artists, Curators, MusicVideos, Playlists, RecordLabels, Songs, Stations.
19 | public let content: SearchSuggestionItem
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Catalog/MCatalog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCatalog.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Structure containing the methods related to the Apple Music catalog.
11 | ///
12 | /// `MCatalog` provides an interface for interacting with the Apple Music catalog.
13 | /// It includes methods for searching, fetching, and managing catalog items like songs, albums, playlists, and more.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// // Fetch a song by its identifier
18 | /// let song = try await MCatalog.song(id: "1613834314")
19 | ///
20 | /// // Search the catalog
21 | /// let results = try await MCatalog.search(for: "the weeknd", types: [.songs, .albums])
22 | /// ```
23 | public struct MCatalog {
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Documentation.docc/Tutorials/table-of-contents.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorials(name: "MusadoraKit") {
2 | @Intro(title: "Learn MusadoraKit") {
3 | Step-by-step guides to help you integrate MusadoraKit into your apps and unlock the full power of Apple Music.
4 | }
5 |
6 | @Chapter(name: "Getting Started") {
7 | @Image(source: "icon.png", alt: "MusadoraKit Icon")
8 |
9 | @TutorialReference(tutorial: "doc:Getting-Started")
10 | }
11 |
12 | @Chapter(name: "Core Features") {
13 | @Image(source: "icon.png", alt: "MusadoraKit Icon")
14 |
15 | @TutorialReference(tutorial: "doc:Working-with-Library")
16 | @TutorialReference(tutorial: "doc:Search-and-Discovery")
17 | }
18 |
19 | @Chapter(name: "Advanced Topics") {
20 | @Image(source: "icon.png", alt: "MusadoraKit Icon")
21 |
22 | @TutorialReference(tutorial: "doc:Building-Music-Player")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Documentation.docc/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rryam/MusadoraKit/28ae04c064cea01cae18f18d470e230325b8fbe4/Sources/MusadoraKit/Documentation.docc/icon.png
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Equivalents/CatalogCleanEquivalent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogCleanEquivalent.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/03/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension EquivalentRequestable {
11 | /// Returns a clean version of the music item.
12 | ///
13 | /// Example usage:
14 | ///
15 | /// let album = try await Album(id: "123").clean
16 | /// let song = try await Song(id: "456").clean
17 | /// let musicVideo = try await MusicVideo(id: "789").clean
18 | ///
19 | var clean: Self {
20 | get async throws {
21 | let path = try EquivalentMusicItemType.path(for: Self.self)
22 | let storefront = try await MusicDataRequest.currentCountryCode
23 |
24 | let url = try cleanEquivalentURL(storefront: storefront, path: path)
25 | let request = MusicDataRequest(urlRequest: .init(url: url))
26 | let response = try await request.response()
27 | let items = try JSONDecoder().decode(MusicItemCollection.self, from: response.data)
28 |
29 | guard let item = items.first else {
30 | throw MusadoraKitError.notFound(for: id.rawValue)
31 | }
32 | return item
33 | }
34 | }
35 |
36 | internal func cleanEquivalentURL(storefront: String, path: EquivalentMusicItemType) throws -> URL {
37 | var components = AppleMusicURLComponents()
38 |
39 | components.path = "catalog/\(storefront)/\(path.rawValue)"
40 |
41 | let filterEquivalentsQuery = URLQueryItem(name: "filter[equivalents]", value: id.rawValue)
42 | let restrictExplicitQuery = URLQueryItem(name: "restrict", value: "explicit")
43 | components.queryItems = [filterEquivalentsQuery, restrictExplicitQuery]
44 |
45 | guard let url = components.url else {
46 | throw URLError(.badURL)
47 | }
48 |
49 | return url
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Equivalents/CatalogCleanEquivalents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogCleanEquivalents.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/03/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension MusicItemCollection where MusicItemType: EquivalentRequestable {
11 |
12 | /// Returns a clean version of the music items.
13 | ///
14 | /// Example usage:
15 | ///
16 | /// let albums: MusicItemCollection = ...
17 | /// let cleanAlbums = try await albums.clean
18 | var clean: Self {
19 | get async throws {
20 | let path = try EquivalentMusicItemType.path(for: MusicItemType.self)
21 | let storefront = try await MusicDataRequest.currentCountryCode
22 |
23 | let url = try cleanEquivalentsURL(storefront: storefront, path: path)
24 | let request = MusicDataRequest(urlRequest: .init(url: url))
25 | let response = try await request.response()
26 | let items = try JSONDecoder().decode(MusicItemCollection.self, from: response.data)
27 | return items
28 | }
29 | }
30 |
31 | internal func cleanEquivalentsURL(storefront: String, path: EquivalentMusicItemType) throws -> URL {
32 | var components = AppleMusicURLComponents()
33 | components.path = "catalog/\(storefront)/\(path.rawValue)"
34 |
35 | let ids = self.map { $0.id.rawValue }.joined(separator: ",")
36 | let filterEquivalentsQuery = URLQueryItem(name: "filter[equivalents]", value: ids)
37 | let restrictExplicitQuery = URLQueryItem(name: "restrict", value: "explicit")
38 |
39 | components.queryItems = [filterEquivalentsQuery, restrictExplicitQuery]
40 |
41 | guard let url = components.url else {
42 | throw URLError(.badURL)
43 | }
44 | return url
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Equivalents/CatalogEquivalents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogEquivalents.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/03/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension MusicItemCollection where MusicItemType: EquivalentRequestable {
11 |
12 | /// Fetches the equivalent music items for the given storefront.
13 | ///
14 | /// Example usage:
15 | ///
16 | /// let albums: MusicItemCollection = ...
17 | /// let equivalentAlbums = try await albums.equivalents(for: "us")
18 | ///
19 | /// - Parameters:
20 | /// - targetStorefront: A string representing the storefront for which the equivalent music items should be fetched.
21 | ///
22 | /// - Returns: A collection of equivalent music items for the given storefront.
23 | func equivalents(for targetStorefront: String) async throws -> MusicItemCollection {
24 | let path = try EquivalentMusicItemType.path(for: MusicItemType.self)
25 |
26 | let url = try equivalentsURL(storefront: targetStorefront, path: path)
27 | let request = MusicDataRequest(urlRequest: .init(url: url))
28 | let response = try await request.response()
29 | let items = try JSONDecoder().decode(MusicItemCollection.self, from: response.data)
30 | return items
31 | }
32 |
33 | internal func equivalentsURL(storefront: String, path: EquivalentMusicItemType) throws -> URL {
34 | var components = AppleMusicURLComponents()
35 |
36 | components.path = "catalog/\(storefront)/\(path.rawValue)"
37 | let ids = self.map { $0.id.rawValue }.joined(separator: ",")
38 | components.queryItems = [URLQueryItem(name: "filter[equivalents]", value: ids)]
39 |
40 | guard let url = components.url else {
41 | throw URLError(.badURL)
42 | }
43 | return url
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Equivalents/EquivalentMusicItemType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EquivalentMusicItemType.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/03/23.
6 | //
7 |
8 | import Foundation
9 | import MusicKit
10 | /// An enum representing the different types of equivalent music items.
11 | enum EquivalentMusicItemType: String, Codable {
12 | case songs
13 | case albums
14 | case musicVideos = "music-videos"
15 | }
16 |
17 | extension EquivalentMusicItemType {
18 |
19 | /// Returns the equivalent music item type for the given type.
20 | ///
21 | /// - Parameter item: A type that conforms to `EquivalentRequestable`.
22 | ///
23 | /// - Throws: An error if the item type is not recognized.
24 | ///
25 | /// - Returns: The equivalent music item type for the given type.
26 | static func path(for item: EquivalentRequestable.Type) throws -> Self {
27 | let path: EquivalentMusicItemType
28 |
29 | switch item {
30 | case is Song.Type: path = .songs
31 | case is Album.Type: path = .albums
32 | case is MusicVideo.Type: path = .musicVideos
33 | default: throw NSError(domain: "Wrong equivalent music item type.", code: 0)
34 | }
35 | return path
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Equivalents/EquivalentRequestable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EquivalentRequestable.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/03/23.
6 | //
7 |
8 | /// A protocol for music items that your app can fetch by
9 | /// using a equivalent request.
10 | public protocol EquivalentRequestable: MusicItem, Codable {
11 | }
12 |
13 | extension Album: EquivalentRequestable {
14 | }
15 |
16 | extension Song: EquivalentRequestable {
17 | }
18 |
19 | extension MusicVideo: EquivalentRequestable {
20 | }
21 |
22 | /// A protocol indicating that a music item can be fetched from a specific storefront using its identifier.
23 | /// Conforming types should provide the resource path component used in the Apple Music API URL.
24 | public protocol StorefrontRequestable: MusicItem, Decodable {
25 |
26 | /// The resource path component used in the Apple Music API URL.
27 | /// For example, for songs, this would be "songs".
28 | static var resourcePath: String { get }
29 | }
30 |
31 | extension Song: StorefrontRequestable {
32 | public static var resourcePath: String { "songs" }
33 | }
34 |
35 | extension Album: StorefrontRequestable {
36 | public static var resourcePath: String { "albums" }
37 | }
38 |
39 | extension MusicVideo: StorefrontRequestable {
40 | public static var resourcePath: String { "music-videos" }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/Array.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 01/08/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Array where Element: Equatable {
11 |
12 | /// Returns a new array with the duplicate elements removed.
13 | ///
14 | /// - Returns: A new array with the duplicate elements removed.
15 | ///
16 | /// - Note: This method has O(n²) time performance, but is optimized for arrays with no more than 10 elements.
17 | func removeDuplicates() -> Self {
18 | reduce(into: []) { result, element in
19 | if !result.contains(element) {
20 | result.append(element)
21 | }
22 | }
23 | }
24 | }
25 |
26 | extension Array where Element == String {
27 |
28 | /// Returns an array of arrays, each containing at most the specified number of elements.
29 | ///
30 | /// - Parameter size: The maximum number of elements in each chunk.
31 | ///
32 | /// - Returns: An array of arrays, each containing at most the specified number of elements.
33 | func chunked(into size: Int) -> [[Element]] {
34 | return stride(from: 0, to: count, by: size).map {
35 | Array(self[$0 ..< Swift.min($0 + size, count)])
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/Artwork.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Artwork.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 10/14/24.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import UIKit
10 | #elseif canImport(AppKit)
11 | import AppKit
12 | #endif
13 |
14 | import Foundation
15 | import SwiftUI
16 |
17 | extension Artwork {
18 |
19 | /// Fetches the artwork image and extracts prominent colors from it.
20 | ///
21 | /// This function downloads the artwork image from a specified URL, processes it to extract
22 | /// the most prominent colors, and returns them as an array of SwiftUI `Color` objects.
23 | ///
24 | /// - Parameters:
25 | /// - width: The desired width of the artwork image.
26 | /// - height: The desired height of the artwork image.
27 | /// - numberOfColors: The number of prominent colors to extract. Default is 9.
28 | ///
29 | /// - Returns: An array of SwiftUI `Color` objects representing the prominent colors.
30 | ///
31 | /// - Throws: An error if the image cannot be fetched or processed.
32 | public func fetchColors(width: Int, height: Int, numberOfColors: Int = 9) async throws -> [Color] {
33 | guard let imageURL = self.url(width: width, height: height) else {
34 | throw NSError(domain: "Invalid artwork URL", code: 0, userInfo: nil)
35 | }
36 |
37 | let (data, _) = try await URLSession.shared.data(from: imageURL)
38 |
39 | #if canImport(UIKit)
40 | guard let image = UIImage(data: data) else {
41 | throw NSError(domain: "Invalid image data", code: 0, userInfo: nil)
42 | }
43 | #elseif canImport(AppKit)
44 | guard let image = NSImage(data: data) else {
45 | throw NSError(domain: "Invalid image data", code: 0, userInfo: nil)
46 | }
47 | #endif
48 |
49 | let colors = try image.extractColors(numberOfColors: numberOfColors)
50 | return colors.map { Color($0) }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/Data.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 14/08/21.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Data {
11 |
12 | /// Returns a formatted JSON string representation of the data.
13 | ///
14 | /// - Throws: An error if the data cannot be decoded.
15 | ///
16 | /// - Returns: A formatted JSON string representation of the data.
17 | func printJSON() throws -> String {
18 | let json = try JSONSerialization.jsonObject(with: self, options: [])
19 | let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
20 |
21 | guard let jsonString = String(data: data, encoding: .utf8) else {
22 | throw URLError(.cannotDecodeRawData)
23 | }
24 | return jsonString
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/NSColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSColor.swift
3 | // MusadoraKit
4 | //
5 | // Created by AI Assistant on 09/02/25.
6 | //
7 |
8 | #if os(macOS)
9 | import AppKit
10 |
11 | extension NSColor {
12 |
13 | /// Converts the NSColor to its hexadecimal string representation.
14 | ///
15 | /// This property provides a convenient way to obtain the hexadecimal string
16 | /// representation of an NSColor. It attempts to convert the color to the sRGB
17 | /// color space before calculating the hex value. If the conversion fails,
18 | /// it returns a default black color hex string.
19 | ///
20 | /// - Returns: A string representing the color in hexadecimal format (#RRGGBB).
21 | /// Returns "#000000" (black) if the color cannot be converted to sRGB.
22 | ///
23 | /// - Note: The returned string is always uppercase and includes the "#" prefix.
24 | var hexString: String {
25 | guard let rgbColor = usingColorSpace(.sRGB) else {
26 | return "#000000"
27 | }
28 |
29 | let r = Int(round(rgbColor.redComponent * 255))
30 | let g = Int(round(rgbColor.greenComponent * 255))
31 | let b = Int(round(rgbColor.blueComponent * 255))
32 |
33 | return String(format: "#%02X%02X%02X", r, g, b)
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/NSImage.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS)
2 | import AppKit
3 |
4 | extension NSImage {
5 |
6 | /// Extracts the most prominent and unique colors from the image.
7 | ///
8 | /// - Parameter numberOfColors: The number of prominent colors to extract (default is 4).
9 | /// - Returns: An array of NSColors representing the prominent colors.
10 | func extractColors(numberOfColors: Int = 4) throws -> [NSColor] {
11 | guard self.cgImage(forProposedRect: nil, context: nil, hints: nil) != nil else {
12 | throw ImageProcessingError.invalidImage
13 | }
14 |
15 | let size = CGSize(width: 200, height: 200 * self.size.height / self.size.width)
16 | let resizedImage = NSImage(size: size)
17 | resizedImage.lockFocus()
18 | self.draw(in: NSRect(origin: .zero, size: size), from: .zero, operation: .copy, fraction: 1.0)
19 | resizedImage.unlockFocus()
20 |
21 | guard let resizedCGImage = resizedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
22 | throw ImageProcessingError.resizeFailed
23 | }
24 |
25 | let colors = try CommonImageProcessing.extractColors(from: resizedCGImage, numberOfColors: numberOfColors)
26 | return colors.map { NSColor(cgColor: $0)! }
27 | }
28 | }
29 | #endif
30 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/UIColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 10/14/24.
6 | //
7 |
8 | #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
9 | import UIKit
10 |
11 | extension UIColor {
12 | /// Converts the UIColor to its hexadecimal string representation.
13 | ///
14 | /// This property provides a convenient way to obtain the hexadecimal string
15 | /// representation of a UIColor. It uses the color's RGB components to calculate
16 | /// the hex value.
17 | ///
18 | /// - Returns: A string representing the color in hexadecimal format (#RRGGBB).
19 | /// Returns "#000000" (black) if the color components cannot be retrieved.
20 | ///
21 | /// - Note: The returned string is always uppercase and includes the "#" prefix.
22 | var hexString: String {
23 | guard let components = self.cgColor.components, components.count >= 3 else {
24 | return "#000000"
25 | }
26 |
27 | let r = Int(components[0] * 255.0)
28 | let g = Int(components[1] * 255.0)
29 | let b = Int(components[2] * 255.0)
30 |
31 | return String(format: "#%02X%02X%02X", r, g, b)
32 | }
33 | }
34 | #endif
35 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Extension/UIImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 10/14/24.
6 | //
7 |
8 | #if os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
9 | import UIKit
10 |
11 | extension UIImage {
12 |
13 | /// Extracts the most prominent and unique colors from the image.
14 | ///
15 | /// - Parameter numberOfColors: The number of prominent colors to extract (default is 4).
16 | /// - Returns: An array of UIColors representing the prominent colors.
17 | func extractColors(numberOfColors: Int = 4) throws -> [UIColor] {
18 | let size = CGSize(width: 200, height: 200 * self.size.height / self.size.width)
19 | UIGraphicsBeginImageContext(size)
20 | self.draw(in: CGRect(origin: .zero, size: size))
21 | guard let resizedImage = UIGraphicsGetImageFromCurrentImageContext() else {
22 | UIGraphicsEndImageContext()
23 | throw ImageProcessingError.resizeFailed
24 | }
25 | UIGraphicsEndImageContext()
26 |
27 | let colors = try CommonImageProcessing.extractColors(from: resizedImage.cgImage!, numberOfColors: numberOfColors)
28 | return colors.map { UIColor(cgColor: $0) }
29 | }
30 | }
31 | #endif
32 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Favorites/Favorites.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Favorites.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 09/02/25.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension MCatalog {
11 |
12 | // MARK: - Public API
13 |
14 | /// Add a song to the user's favorites.
15 | ///
16 | /// - Parameter song: The song to add to favorites.
17 | /// - Returns: A boolean value indicating whether the operation was successful.
18 | @discardableResult static func favorite(song: Song) async throws -> Bool {
19 | try await addToFavorites(id: song.id)
20 | }
21 |
22 | /// Add an album to the user's favorites.
23 | ///
24 | /// - Parameter album: The album to add to favorites.
25 | /// - Returns: A boolean value indicating whether the operation was successful.
26 | @discardableResult static func favorite(album: Album) async throws -> Bool {
27 | try await addToFavorites(id: album.id)
28 | }
29 |
30 | /// Add a playlist to the user's favorites.
31 | ///
32 | /// - Parameter playlist: The playlist to add to favorites.
33 | /// - Returns: A boolean value indicating whether the operation was successful.
34 | @discardableResult static func favorite(playlist: Playlist) async throws -> Bool {
35 | try await addToFavorites(id: playlist.id)
36 | }
37 |
38 | /// Add an artist to the user's favorites.
39 | ///
40 | /// - Parameter artist: The artist to add to favorites.
41 | /// - Returns: A boolean value indicating whether the operation was successful.
42 | @discardableResult static func favorite(artist: Artist) async throws -> Bool {
43 | try await addToFavorites(id: artist.id)
44 | }
45 |
46 | /// Add a music video to the user's favorites.
47 | ///
48 | /// - Parameter musicVideo: The music video to add to favorites.
49 | /// - Returns: A boolean value indicating whether the operation was successful.
50 | @discardableResult static func favorite(musicVideo: MusicVideo) async throws -> Bool {
51 | try await addToFavorites(id: musicVideo.id)
52 | }
53 |
54 | /// Add a station to the user's favorites.
55 | ///
56 | /// - Parameter station: The station to add to favorites.
57 | /// - Returns: A boolean value indicating whether the operation was successful.
58 | @discardableResult static func favorite(station: Station) async throws -> Bool {
59 | try await addToFavorites(id: station.id)
60 | }
61 |
62 |
63 | // MARK: - Internal Implementation
64 |
65 | /// Internal method to add a resource to favorites by ID.
66 | ///
67 | /// - Parameter id: The identifier of the music item to add to favorites.
68 | /// - Returns: A boolean value indicating whether the operation was successful.
69 | @discardableResult static func addToFavorites(id: MusicItemID) async throws -> Bool {
70 | let request = MFavoritesRequest(itemID: id)
71 | return try await request.response()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Favorites/MFavoritesRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MFavoritesRequest.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 09/02/25.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A request that your app uses to add resources to favorites.
11 | struct MFavoritesRequest {
12 | private var itemIDs: [MusicItemID]
13 |
14 | /// Creates a request to add resources to favorites.
15 | ///
16 | /// - Parameter itemIDs: The IDs of the items to add to favorites.
17 | /// Supports heterogeneous types (songs, albums, playlists, artists, etc.)
18 | init(itemIDs: [MusicItemID]) {
19 | self.itemIDs = itemIDs
20 | }
21 |
22 | /// Creates a request to add a single resource to favorites.
23 | ///
24 | /// - Parameter itemID: The ID of the item to add to favorites.
25 | init(itemID: MusicItemID) {
26 | self.itemIDs = [itemID]
27 | }
28 |
29 | /// Executes the request to add items to favorites and returns
30 | /// a boolean value indicating the success of the operation.
31 | func response() async throws -> Bool {
32 | let url = try favoritesEndpointURL
33 | let request = MDataPostRequest(url: url)
34 | let response = try await request.response()
35 | return response.urlResponse.statusCode == 202
36 | }
37 | }
38 |
39 | extension MFavoritesRequest {
40 | internal var favoritesEndpointURL: URL {
41 | get throws {
42 | var components = AppleMusicURLComponents()
43 | var queryItems: [URLQueryItem] = []
44 | components.path = "me/favorites"
45 |
46 | // Join all item IDs with commas as per official API spec
47 | let idsString = itemIDs.map { $0.rawValue }.joined(separator: ",")
48 | queryItems.append(URLQueryItem(name: "ids", value: idsString))
49 |
50 | components.queryItems = queryItems
51 |
52 | guard let url = components.url else {
53 | throw URLError(.badURL)
54 | }
55 |
56 | return url
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/History/MHistory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MHistory.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 15/03/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Structure containing the methods for accessing historical data.
11 | ///
12 | /// `MHistory` provides access to a user's music listening history, including
13 | /// recently played items and recently added content.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// // Fetch recently played items
18 | /// let recentlyPlayed = try await MHistory.recentlyPlayed()
19 | /// print(recentlyPlayed.songs)
20 | /// print(recentlyPlayed.albums)
21 | ///
22 | /// // Fetch recently added items
23 | /// let recentlyAdded = try await MHistory.recentlyAdded()
24 | /// print(recentlyAdded.playlists)
25 | /// ```
26 | public struct MHistory {
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/History/MHistoryEndpoints.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MHistoryEndpoints.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 02/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Different endpoints related to historial data.
11 | /// Possible types: Heavy rotation, recently added, and recently played resources.
12 | enum MHistoryEndpoints {
13 | case heavyRotation
14 | case recentlyAdded
15 | case recentlyPlayed
16 | case recentlyPlayedTracks
17 | case recentlyPlayedStations
18 |
19 | var path: String {
20 | switch self {
21 | case .heavyRotation:
22 | return "history/heavy-rotation"
23 | case .recentlyAdded:
24 | return "library/recently-added"
25 | case .recentlyPlayed:
26 | return "recent/played"
27 | case .recentlyPlayedTracks:
28 | return "recent/played/tracks"
29 | case .recentlyPlayedStations:
30 | return "recent/radio-stations"
31 | }
32 | }
33 |
34 | var maximumLimit: Int {
35 | switch self {
36 | case .heavyRotation, .recentlyPlayed, .recentlyPlayedStations:
37 | return 10
38 | case .recentlyAdded:
39 | return 25
40 | case .recentlyPlayedTracks:
41 | return 30
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/History/MHistoryResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MHistoryResponse.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 02/04/22.
6 | //
7 |
8 | /// An object that contains results for a history request.
9 | ///
10 | /// This structure provides access to a user's historical music activity,
11 | /// including recently played items and recently added content.
12 | ///
13 | /// Example usage:
14 | /// ```swift
15 | /// let request = MHistoryRequest()
16 | /// let response = try await request.response()
17 | ///
18 | /// // Access historical items by type
19 | /// print(response.albums) // Recently played albums
20 | /// print(response.tracks) // Recently played tracks
21 | /// print(response.playlists) // Recently played playlists
22 | /// ```
23 | struct MHistoryResponse {
24 | /// A collection of historical resources based on the `MusicHistoryRequest`.
25 | let items: UserMusicItems
26 |
27 | /// A collection of historical albums.
28 | var albums: Albums {
29 | MusicItemCollection(
30 | items.compactMap { item in
31 | guard case let .album(album) = item else { return nil }
32 | return album
33 | })
34 | }
35 |
36 | /// A collection of historical playlists.
37 | var playlists: Playlists {
38 | MusicItemCollection(
39 | items.compactMap { item in
40 | guard case let .playlist(playlist) = item else { return nil }
41 | return playlist
42 | })
43 | }
44 |
45 | /// A collection of historical stations.
46 | var stations: Stations {
47 | MusicItemCollection(
48 | items.compactMap { item in
49 | guard case let .station(station) = item else { return nil }
50 | return station
51 | })
52 | }
53 |
54 | /// A collection of historical tracks.
55 | var tracks: Tracks {
56 | MusicItemCollection(
57 | items.compactMap { item in
58 | guard case let .track(track) = item else { return nil }
59 | return track
60 | })
61 | }
62 | }
63 |
64 | extension MHistoryResponse: Equatable, Hashable, Codable {}
65 |
66 | extension MHistoryResponse: CustomStringConvertible {
67 | var description: String {
68 | "MusicHistoryResponse(\(items.description)"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/History/MusicRecentlyAddedRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicRecentlyAddedRequest.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 30/06/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A protocol for music items that your app can fetch by
11 | /// using a recently added request.
12 | public protocol MRecentlyAddedRequestable: MusicItem {
13 | }
14 |
15 | /// A request that your app uses to fetch items the user has recently added.
16 | public struct MRecentlyAddedRequest
17 | where MusicItemType: MRecentlyAddedRequestable, MusicItemType: Decodable {
18 |
19 | /// Creates a request for items the user has recently added.
20 | public init() {}
21 |
22 | /// A limit for the number of items to return
23 | /// in the response that contains items the user has recently added.
24 | public var limit: Int?
25 |
26 | /// An offet for the request.
27 | public var offset: Int?
28 |
29 | /// A list of properties which the recently added request
30 | /// will fetch for each music item in the response.
31 | public var properties: [PartialMusicAsyncProperty] = []
32 |
33 | /// Fetches items the user has recently added.
34 | // public func response() async throws -> MusicRecentlyAddedResponse {
35 | // return MusicRecentlyAddedResponse(items: .init(arrayLiteral: []))
36 | // }
37 | }
38 |
39 | /// A response that contains items the user has recently added to their library.
40 | ///
41 | /// This structure provides access to a collection of music items that have been recently
42 | /// added to the user's Apple Music library. The items can be of any type that conforms
43 | /// to the `MRecentlyAddedRequestable` protocol.
44 | ///
45 | /// Example usage:
46 | /// ```swift
47 | /// let request = MRecentlyAddedRequest()
48 | ///
49 | /// do {
50 | /// let response = try await request.response()
51 | /// for song in response.items {
52 | /// print("Recently added song: \(song.title)")
53 | /// }
54 | /// } catch {
55 | /// print("Failed to fetch recently added items: \(error)")
56 | /// }
57 | /// ```
58 | public struct MRecentlyAddedResponse where MusicItemType: MRecentlyAddedRequestable {
59 |
60 | /// A collection of items the user has recently added.
61 | ///
62 | /// This property contains the music items that were recently added to the user's library,
63 | /// ordered by the date they were added (most recent first).
64 | public let items: MusicItemCollection
65 | }
66 |
67 | extension MRecentlyAddedResponse: Sendable {
68 | }
69 |
70 | extension MRecentlyAddedResponse: Decodable where MusicItemType: Decodable {
71 | }
72 |
73 | extension MRecentlyAddedResponse: Encodable where MusicItemType: Encodable {
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/LibraryCatalog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryCatalog.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 10/05/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension FilterableLibraryItem {
11 |
12 | /// The catalog version of the library item.
13 | var catalog: Self {
14 | get async throws {
15 | let path = try LibraryMusicItemType.path(for: Self.self)
16 | let url = try catalogURL(path: path)
17 | let decoder = JSONDecoder()
18 |
19 | if let userToken = MusadoraKit.userToken {
20 | let request = MUserRequest(urlRequest: .init(url: url), userToken: userToken)
21 | let data = try await request.response()
22 | let items = try decoder.decode(MusicItemCollection.self, from: data)
23 |
24 | guard let item = items.first else {
25 | throw MusadoraKitError.notFound(for: id.rawValue)
26 | }
27 |
28 | return item
29 | } else {
30 | let request = MusicDataRequest(urlRequest: URLRequest(url: url))
31 | let response = try await request.response()
32 | let items = try decoder.decode(MusicItemCollection.self, from: response.data)
33 |
34 | guard let item = items.first else {
35 | throw MusadoraKitError.notFound(for: id.rawValue)
36 | }
37 |
38 | return item
39 | }
40 | }
41 | }
42 |
43 | internal func catalogURL(path: LibraryMusicItemType) throws -> URL {
44 | var components = AppleMusicURLComponents()
45 | components.path = "me/library/\(path.rawValue)/\(id.rawValue)/catalog"
46 |
47 | guard let url = components.url else {
48 | throw URLError(.badURL)
49 | }
50 |
51 | return url
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/LibraryGenre.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryGenre.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 29/07/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension MLibrary {
11 |
12 | /// Fetch all genres from the user's library.
13 | ///
14 | /// Use this method to retrieve all the genres present in the user's music library. Example of the genres are Alternative, Rock, etc.
15 | ///
16 | /// Example usage:
17 | ///
18 | /// let genres = try await MLibrary.genres()
19 | /// for genre in genres {
20 | /// print("Genre: \(genre.name)")
21 | /// }
22 | /// // ... access other properties
23 | ///
24 | /// - Returns: A `Genres` collection containing all the genres present in the user's library.
25 | /// - Throws: An error if the retrieval fails, such as network connectivity issues or invalid parameters.
26 | ///
27 | /// - Note: This method fetches the genres locally from the device,
28 | /// and is faster because it uses the latest `MusicLibraryRequest` structure.
29 | @available(iOS 16.0, macOS 14.0, macCatalyst 17.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
30 | static func genres() async throws -> Genres {
31 | let request = MusicLibraryRequest()
32 | let response = try await request.response()
33 | return response.items
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/MLibrary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibrary.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 24/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Structure containing the methods related to the user's library.
11 | ///
12 | /// `MLibrary` provides comprehensive access to a user's Apple Music library,
13 | /// including methods to fetch, search, and manage library content.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// // Fetch all library songs
18 | /// let songs = try await MLibrary.songs()
19 | ///
20 | /// // Search the library
21 | /// let searchResults = try await MLibrary.search(for: "coldplay", types: [Song.self])
22 | ///
23 | /// // Add a playlist to library
24 | /// try await MLibrary.addPlaylist(name: "My Playlist", description: "My favorite songs")
25 | /// ```
26 | public struct MLibrary {
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/MusicLibrarySearchable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibrarySearchable.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 08/09/21.
6 | //
7 |
8 | /// A protocol representing music items that can be fetched through a library search request.
9 | ///
10 | /// Adhere to this protocol to enable library searching functionality for custom music items.
11 | /// By default, the protocol provides a unique identifier for each type that conforms to it.
12 | ///
13 | /// Example:
14 | ///
15 | /// if let searchableItem = someMusicItem as? MLibrarySearchable {
16 | /// let identifier = searchableItem.searchIdentifier
17 | /// }
18 | ///
19 | public protocol MLibrarySearchable: MusicItem {}
20 |
21 | extension MLibrarySearchable {
22 |
23 | /// A unique identifier associated with the type that conforms to `MLibrarySearchable`.
24 | ///
25 | /// This identifier can be useful for distinguishing different types of music items during search operations or other tasks.
26 | static var searchIdentifier: ObjectIdentifier {
27 | ObjectIdentifier(Self.self)
28 | }
29 | }
30 |
31 | /// `Song` conformance to `MLibrarySearchable`, allowing songs to be fetched using library search requests.
32 | extension Song: MLibrarySearchable {}
33 |
34 | /// `Artist` conformance to `MLibrarySearchable`, allowing artists to be fetched using library search requests.
35 | extension Artist: MLibrarySearchable {}
36 |
37 | /// `Album` conformance to `MLibrarySearchable`, allowing albums to be fetched using library search requests.
38 | extension Album: MLibrarySearchable {}
39 |
40 | /// `MusicVideo` conformance to `MLibrarySearchable`, allowing music videos to be fetched using library search requests.
41 | extension MusicVideo: MLibrarySearchable {}
42 |
43 | /// `Playlist` conformance to `MLibrarySearchable`, allowing playlists to be fetched using library search requests.
44 | extension Playlist: MLibrarySearchable {}
45 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Playlist Folder Request/LibraryPlaylistFolder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryPlaylistFolder.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 26/12/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A structure representing a playlist folder in the user's library.
11 | ///
12 | /// `LibraryPlaylistFolder` provides information about a folder that contains playlists
13 | /// in the user's Apple Music library, including its metadata and relationships.
14 | ///
15 | /// Example usage:
16 | /// ```swift
17 | /// let folder = try await MLibrary.playlistFolder(id: "folder_id")
18 | /// print(folder.attributes?.name)
19 | /// print(folder.attributes?.dateAdded)
20 | /// ```
21 | public struct LibraryPlaylistFolder: Codable, Sendable {
22 | /// The unique identifier of the playlist folder.
23 | public let id: String
24 |
25 | /// The type of the resource, typically "library-playlist-folders".
26 | public let type: String
27 |
28 | /// The attributes of the playlist folder.
29 | public let attributes: Attributes?
30 |
31 | /// The relationships of the playlist folder.
32 | public let relationships: Relationships?
33 |
34 | /// The attributes associated with a playlist folder.
35 | public struct Attributes: Codable, Sendable {
36 | /// The date when the folder was added to the library.
37 | public let dateAdded: Date
38 |
39 | /// The name of the playlist folder.
40 | public let name: String
41 | }
42 |
43 | /// The relationships associated with a playlist folder.
44 | public struct Relationships: Codable, Sendable {
45 | // Reserved for future relationships
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Playlist Folder Request/MLibraryPlaylistFolder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibraryPlaylistFolder.swift
3 | //
4 | //
5 | // Created by Rudrank Riyam on 26/12/22.
6 | //
7 |
8 | /// A collection of playlist folders from the user's library.
9 | ///
10 | /// This type alias represents an array of playlist folders that can be retrieved from
11 | /// the user's Apple Music library. Each folder can contain multiple playlists and
12 | /// can be organized hierarchically.
13 | ///
14 | /// Example usage:
15 | /// ```swift
16 | /// do {
17 | /// let folders: LibraryPlaylistFolders = try await MLibrary.playlistFolders()
18 | /// for folder in folders {
19 | /// print("Folder name: \(folder.name)")
20 | /// print("Number of playlists: \(folder.playlists.count)")
21 | /// }
22 | /// } catch {
23 | /// print("Failed to fetch playlist folders: \(error)")
24 | /// }
25 | /// ```
26 | public typealias LibraryPlaylistFolders = [LibraryPlaylistFolder]
27 |
28 | extension MLibrary {
29 |
30 | /// Fetches all playlist folders from the user's library.
31 | ///
32 | /// This method retrieves the root-level playlist folders from the user's Apple Music library.
33 | /// Each folder may contain playlists and subfolders.
34 | ///
35 | /// Example usage:
36 | /// ```swift
37 | /// do {
38 | /// let folders = try await MLibrary.rootPlaylistsFolder()
39 | /// for folder in folders {
40 | /// print("Found folder: \(folder.name)")
41 | /// }
42 | /// } catch {
43 | /// print("Error fetching playlist folders: \(error)")
44 | /// }
45 | /// ```
46 | ///
47 | /// - Returns: An array of `LibraryPlaylistFolder` objects representing the user's playlist folders.
48 | /// - Throws: An error if the request fails or if the user's authorization status has changed.
49 | // static func rootPlaylistsFolder() async throws -> LibraryPlaylistFolders {
50 | // Implementation commented out
51 | // }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Resource Request/LibraryMusicItemType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryMusicItemType.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 18/05/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Set the `itemType` to indicate the type of library item for which you want to fetch ratings.
11 | ///
12 | /// ### Usage Example:
13 | ///
14 | /// ```swift
15 | /// let itemType: LibraryMusicItemType = .song
16 | ///
17 | /// do {
18 | /// let ratings = try await MLibraryRatingRequest(with: ids, item: itemType).response()
19 | /// // Use the ratings for further processing.
20 | /// } catch {
21 | /// // Handle the error.
22 | /// }
23 | /// ```
24 | public enum LibraryMusicItemType: String, Codable {
25 |
26 | /// Represents songs in the music library.
27 | case songs
28 |
29 | /// Represents playlists in the music library.
30 | case playlists
31 |
32 | /// Represents albums in the music library.
33 | case albums
34 |
35 | /// Represents artists in the music library.
36 | case artists
37 |
38 | /// Represents music videos in the music library with a raw value of "music-videos".
39 | case musicVideos = "music-videos"
40 |
41 | /// A computed property that generates a type string for the item.
42 | ///
43 | /// The type string is used in requests to identify the specific item type.
44 | public var type: String {
45 | "ids[\(rawValue)]".removingPercentEncoding!
46 | }
47 | }
48 |
49 | /// An extension providing utility methods for `LibraryMusicItemType`.
50 | extension LibraryMusicItemType {
51 | /// Returns the `LibraryMusicItemType` corresponding to a given type conforming to `FilterableLibraryItem`.
52 | ///
53 | /// Use this method to map a type to the appropriate library item type for operations involving music library items.
54 | ///
55 | /// - Parameter item: The type conforming to `FilterableLibraryItem`.
56 | /// - Returns: The `LibraryMusicItemType` corresponding to the provided type.
57 | ///
58 | /// - Throws: An error if the input type does not match any known library item types.
59 | public static func path(for item: any FilterableLibraryItem.Type) throws -> Self {
60 | let path: LibraryMusicItemType
61 |
62 | switch item {
63 | case is Song.Type:
64 | path = .songs
65 | case is Album.Type:
66 | path = .albums
67 | case is Artist.Type:
68 | path = .artists
69 | case is MusicVideo.Type:
70 | path = .musicVideos
71 | case is Playlist.Type:
72 | path = .playlists
73 | default:
74 | throw NSError(domain: "Wrong library music item type.", code: 0)
75 | }
76 |
77 | return path
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Resource Request/MLibraryResourceResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibraryResourceResponse.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 02/04/22.
6 | //
7 |
8 | /// An object that contains results for a library resource request.
9 | struct MLibraryResourceResponse where MusicItemType: MusicItem {
10 | /// A collection of items matching the filter used in
11 | /// the originating ``MusicLibraryResourceRequest``.
12 | let items: MusicItemCollection
13 | }
14 |
15 | extension MLibraryResourceResponse: Equatable where MusicItemType: Equatable {}
16 |
17 | extension MLibraryResourceResponse: Hashable where MusicItemType: Hashable {}
18 |
19 | extension MLibraryResourceResponse: Decodable where MusicItemType: Decodable {
20 | /// Creates a new instance by decoding from the given decoder.
21 | ///
22 | /// This initializer throws an error if reading from the decoder fails, or
23 | /// if the data read is corrupted or otherwise invalid.
24 | ///
25 | /// - Parameter decoder: The decoder to read data from.
26 | // init(from decoder: Decoder) throws {
27 | //
28 | // }
29 | }
30 |
31 | extension MLibraryResourceResponse: Encodable where MusicItemType: Encodable {
32 | /// Encodes this value into the given encoder.
33 | ///
34 | /// If the value fails to encode anything, `encoder` will encode an empty
35 | /// keyed container in its place.
36 | ///
37 | /// This function throws an error if any values are invalid for the given
38 | /// encoder's format.
39 | ///
40 | /// - Parameter encoder: The encoder to write data to.
41 | // func encode(to encoder: Encoder) throws { }
42 | }
43 |
44 | extension MLibraryResourceResponse: CustomStringConvertible, CustomDebugStringConvertible {
45 | var description: String {
46 | ""
47 | }
48 |
49 | var debugDescription: String {
50 | ""
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Search Request/MusicLibrarySearchResponseResults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibrarySearchResponseResults.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 08/09/21.
6 | //
7 |
8 | /// The results from the library search request using a search term.
9 | ///
10 | /// This structure wraps the search response from the Apple Music API when searching
11 | /// a user's library. It provides access to the search results through its `results` property.
12 | ///
13 | /// Example usage:
14 | /// ```swift
15 | /// let request = MLibrarySearchRequest(term: "coldplay", types: [Song.self])
16 | /// let response = try await request.response()
17 | /// let results = MLibrarySearchResponseResults(results: response)
18 | /// print(results.results.songs)
19 | /// ```
20 | struct MLibrarySearchResponseResults: Decodable {
21 | /// The search response containing collections of different music items.
22 | var results: MLibrarySearchResponse
23 | }
24 |
25 | extension MLibrarySearchResponseResults: CustomStringConvertible, CustomDebugStringConvertible {
26 | public var description: String {
27 | var description = "MusicLibrarySearchResponseResults("
28 | let mirror = Mirror(reflecting: self)
29 |
30 | description += mirror.children.map { "\n\($0.value)," }.joined()
31 |
32 | return description + "\n)"
33 | }
34 |
35 | public var debugDescription: String {
36 | "MusicLibrarySearchResponseResults(\n\(results))"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Library/Search Request/MusicLibrarySearchType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MLibrarySearchType.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 08/09/21.
6 | //
7 |
8 | enum MLibrarySearchType: String, CodingKey {
9 | case songs = "library-songs"
10 | case artists = "library-artists"
11 | case albums = "library-albums"
12 | case musicVideos = "library-music-videos"
13 | case playlists = "library-playlists"
14 |
15 | static func getTypes(_ types: [MLibrarySearchable.Type]) -> String {
16 | types
17 | .map { $0.searchIdentifier }
18 | .removeDuplicates()
19 | .compactMap {
20 | switch $0 {
21 | case Song.searchIdentifier: return songs.rawValue
22 | case Album.searchIdentifier: return albums.rawValue
23 | case MusicVideo.searchIdentifier: return musicVideos.rawValue
24 | case Playlist.searchIdentifier: return playlists.rawValue
25 | case Artist.searchIdentifier: return artists.rawValue
26 | default: return nil
27 | }
28 | }
29 | .joined(separator: ",")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/AppleMusicURLComponents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleMusicURLComponents.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 31/07/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A protocol for URL components that can be used to construct URLs for API requests.
11 | public protocol MURLComponents {
12 |
13 | /// The query items to include in the URL.
14 | var queryItems: [URLQueryItem]? { get set }
15 |
16 | /// The path for the URL, excluding the base path.
17 | var path: String { get set }
18 |
19 | /// The constructed URL, if valid.
20 | var url: URL? { get }
21 | }
22 |
23 | /// A structure that implements the `MURLComponents` protocol, specifically for Apple Music API requests.
24 | public struct AppleMusicURLComponents: MURLComponents {
25 |
26 | /// The underlying `URLComponents` instance.
27 | private var components: URLComponents
28 |
29 | /// Initializes a new `AppleMusicURLComponents` instance with default values for the scheme and host.
30 | public init() {
31 | self.components = URLComponents()
32 | components.scheme = "https"
33 | components.host = "api.music.apple.com"
34 | }
35 |
36 | /// The query items to include in the URL.
37 | public var queryItems: [URLQueryItem]? {
38 | get {
39 | components.queryItems
40 | } set {
41 | components.queryItems = newValue
42 | }
43 | }
44 |
45 | /// The path for the URL, excluding the base path.
46 | public var path: String {
47 | get {
48 | return components.path
49 | } set {
50 | components.path = "/v1/" + newValue
51 | }
52 | }
53 |
54 | /// The constructed URL, if valid.
55 | public var url: URL? {
56 | components.url
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/RelationshipItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelationshipItem.swift
3 | // RelationshipItem
4 | //
5 | // Created by Rudrank Riyam on 04/08/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// An enum representing the types of items that can be related to a music item.
11 | public enum RelationshipItem: String {
12 | case albums
13 | case artists
14 | case composers
15 | case genres
16 | case library
17 | case musicVideos = "music-videos"
18 | case station
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/SongsForAlbums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongsForAlbums.swift
3 | //
4 | //
5 | // Created by Rudrank Riyam on 02/08/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A type alias representing a collection of songs for multiple albums.
11 | ///
12 | /// This alias is used to manage and manipulate a collection of `SongsForAlbums` items.
13 | @available(iOS 16.0, macOS 14.0, macCatalyst 17.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
14 | public typealias SongsForAlbums = [MusicLibrarySection]
15 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/SongsForArtists.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongsForArtists.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 02/08/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A type alias representing a collection of songs for multiple artists.
11 | ///
12 | /// This alias is used to manage and manipulate a collection of `SongsForArtists` items.
13 | @available(iOS 16.0, macOS 14.0, macCatalyst 17.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
14 | public typealias SongsForArtists = [MusicLibrarySection]
15 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/SongsForGenres.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongsGenres.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 01/08/23.
6 | //
7 |
8 | /// A type alias representing a collection of songs for multiple genres.
9 | ///
10 | /// This alias is used to manage and manipulate a collection of `SongsForGenre` items.
11 | @available(iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
12 | @available(macOS, unavailable)
13 | @available(macCatalyst, unavailable)
14 | public typealias SongsForGenres = [MusicLibrarySection]
15 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/SongsForPlaylists.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongsForPlaylists.swift
3 | //
4 | //
5 | // Created by Rudrank Riyam on 02/08/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A type alias representing a collection of songs for multiple playlists.
11 | ///
12 | /// This alias is used to manage and manipulate a collection of `SongsForPlaylists` items.
13 | @available(iOS 16.0, macOS 14.0, macCatalyst 17.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
14 | public typealias SongsForPlaylists = [MusicLibrarySection]
15 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Models/StationGenre.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationGenre.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 12/03/23.
6 | //
7 |
8 | /// A structure representing a station genre in the Apple Music API.
9 | ///
10 | /// Station genres are used to categorize and organize radio stations in Apple Music.
11 | /// Each genre has a unique identifier, type, and display name.
12 | ///
13 | /// Example usage:
14 | /// ```swift
15 | /// let genre = try await MCatalog.stationGenre(id: "42")
16 | /// print(genre.name) // e.g., "Hip-Hop"
17 | /// print(genre.id) // The unique identifier
18 | /// ```
19 | public struct StationGenre: Sendable {
20 |
21 | /// The unique identifier of the station genre.
22 | public let id: MusicItemID
23 |
24 | /// The type of the station genre.
25 | public let type: String
26 |
27 | /// The display name of the station genre.
28 | public let name: String
29 | }
30 |
31 | /// An extension that adds `Decodable` conformance to the `StationGenre` structure.
32 | extension StationGenre: Decodable {
33 |
34 | /// An enumeration of the top-level coding keys.
35 | enum CodingKeys: String, CodingKey {
36 | case attributes, id, type
37 | }
38 |
39 | /// An enumeration of the attribute-level coding keys.
40 | enum AttributesKey: String, CodingKey {
41 | case name
42 | }
43 |
44 | /// Initializes a new `StationGenre` instance from the given decoder.
45 | public init(from decoder: Decoder) throws {
46 | let container = try decoder.container(keyedBy: CodingKeys.self)
47 |
48 | id = try container.decode(MusicItemID.self, forKey: .id)
49 | type = try container.decode(String.self, forKey: .type)
50 |
51 | let attributesContainer = try container.nestedContainer(
52 | keyedBy: AttributesKey.self, forKey: .attributes)
53 | name = try attributesContainer.decode(String.self, forKey: .name)
54 | }
55 | }
56 |
57 | /// An extension that adds `MusicItem` conformance to the `StationGenre` structure.
58 | extension StationGenre: MusicItem {}
59 |
60 | /// An extension that adds `Equatable`, `Hashable`, and `Identifiable` conformances to the `StationGenre` structure.
61 | extension StationGenre: Equatable, Hashable, Identifiable {
62 |
63 | /// Determines if two `StationGenre` instances are equal based on their identifiers.
64 | public static func == (lhs: StationGenre, rhs: StationGenre) -> Bool {
65 | lhs.id == rhs.id
66 | }
67 |
68 | /// Computes the hash value for a `StationGenre` instance based on its identifier.
69 | public func hash(into hasher: inout Hasher) {
70 | hasher.combine(id)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Multiple Resources/MusicCatalogResources/MusicCatalogResourcesRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicCatalogResourcesRequest.swift
3 | // MusicCatalogResourcesRequest
4 | //
5 | // Created by Rudrank Riyam on 22/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A request that your app uses to fetch multiple resources from the Apple Music catalog
11 | /// using their identifiers.
12 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
13 | public struct MusicCatalogResourcesRequest {
14 | private var types: [MusicCatalogResourcesType.Key: [MusicItemID]]
15 |
16 | /// Creates a request to fetch multiple resources from the Apple Music catalog using their identifiers.
17 | public init(types: [MusicCatalogResourcesType.Key: [MusicItemID]]) {
18 | self.types = types
19 | }
20 |
21 | /// Fetches different catalog music items based on the types for the given request.
22 | public func response() async throws -> MusicCatalogResourcesResponse {
23 | let url = try await multipleCatalogResourcesEndpointURL
24 | let request = MusicDataRequest(urlRequest: .init(url: url))
25 | let response = try await request.response()
26 | let items = try JSONDecoder().decode(MusicCatalogResourcesTypes.self, from: response.data)
27 | return MusicCatalogResourcesResponse(items: items)
28 | }
29 | }
30 |
31 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
32 | extension MusicCatalogResourcesRequest {
33 | private var multipleCatalogResourcesEndpointURL: URL {
34 | get async throws {
35 | let storefront = try await MusicDataRequest.currentCountryCode
36 |
37 | var components = AppleMusicURLComponents()
38 | var queryItems: [URLQueryItem] = []
39 | components.path = "catalog/\(storefront)"
40 |
41 | for (key, value) in types {
42 | let values = value.map { $0.rawValue }.joined(separator: ",")
43 | queryItems.append(URLQueryItem(name: key.type, value: values))
44 | }
45 |
46 | components.queryItems = queryItems
47 |
48 | guard let url = components.url else {
49 | throw URLError(.badURL)
50 | }
51 | return url
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Multiple Resources/MusicCatalogResources/MusicCatalogResourcesResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicCatalogResourcesResponse.swift
3 | // MusicCatalogResourcesResponse
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// An object that contains results for a catalog resources request.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public struct MusicCatalogResourcesResponse {
11 |
12 | /// A collection of stations.
13 | public var stations: Stations {
14 | MusicItemCollection(items.compactMap { item in
15 | guard case let .station(station) = item else { return nil }
16 | return station
17 | })
18 | }
19 |
20 | /// A collection of genres.
21 | public var genres: Genres {
22 | MusicItemCollection(items.compactMap { item in
23 | guard case let .genre(genre) = item else { return nil }
24 | return genre
25 | })
26 | }
27 |
28 | /// A collection of recordLabels.
29 | public var recordLabels: RecordLabels {
30 | MusicItemCollection(items.compactMap { item in
31 | guard case let .recordLabel(recordLabel) = item else { return nil }
32 | return recordLabel
33 | })
34 | }
35 |
36 | /// A collection of playlists.
37 | public var playlists: Playlists {
38 | MusicItemCollection(items.compactMap { item in
39 | guard case let .playlist(playlist) = item else { return nil }
40 | return playlist
41 | })
42 | }
43 |
44 | /// A collection of artists.
45 | public var artists: Artists {
46 | MusicItemCollection(items.compactMap { item in
47 | guard case let .artist(artist) = item else { return nil }
48 | return artist
49 | })
50 | }
51 |
52 | /// A collection of albums.
53 | public var albums: Albums {
54 | MusicItemCollection(items.compactMap { item in
55 | guard case let .album(album) = item else { return nil }
56 | return album
57 | })
58 | }
59 |
60 | /// A collection of curators.
61 | public var curators: Curators {
62 | MusicItemCollection(items.compactMap { item in
63 | guard case let .curator(curator) = item else { return nil }
64 | return curator
65 | })
66 | }
67 |
68 | /// A collection of radio shows.
69 | public var radioShows: RadioShows {
70 | MusicItemCollection(items.compactMap { item in
71 | guard case let .radioShow(radioShow) = item else { return nil }
72 | return radioShow
73 | })
74 | }
75 |
76 | /// A collection of music videos.
77 | public var musicVideos: MusicVideos {
78 | MusicItemCollection(items.compactMap { item in
79 | guard case let .musicVideo(musicVideo) = item else { return nil }
80 | return musicVideo
81 | })
82 | }
83 |
84 | /// A collection of songs.
85 | public var songs: Songs {
86 | MusicItemCollection(items.compactMap { item in
87 | guard case let .song(song) = item else { return nil }
88 | return song
89 | })
90 | }
91 |
92 | /// A collection of different music items.
93 | var items: MusicCatalogResourcesTypes
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Multiple Resources/MusicLibraryResources/MusicLibraryResourcesRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicLibraryResourcesRequest.swift
3 | // MusicLibraryResourcesRequest
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A request that your app uses to fetch multiple resources from the user's library
11 | /// using their identifiers.
12 | public struct MusicLibraryResourcesRequest {
13 | private var types: [MusicLibraryResourcesType.Key: [MusicItemID]]
14 |
15 | /// Creates a request to fetch multiple resources from the user's library using their identifiers.
16 | public init(types: [MusicLibraryResourcesType.Key: [MusicItemID]]) {
17 | self.types = types
18 | }
19 |
20 | /// Fetches different library music items based on the types for the given request.
21 | public func response() async throws -> MusicLibraryResourcesResponse {
22 | let url = try multipleLibraryResourcesEndpointURL
23 | let request = MusicDataRequest(urlRequest: .init(url: url))
24 | let response = try await request.response()
25 | let items = try JSONDecoder().decode(MusicLibraryResourcesTypes.self, from: response.data)
26 | return MusicLibraryResourcesResponse(items: items)
27 | }
28 | }
29 |
30 | extension MusicLibraryResourcesRequest {
31 | private var multipleLibraryResourcesEndpointURL: URL {
32 | get throws {
33 | var components = AppleMusicURLComponents()
34 | var queryItems: [URLQueryItem] = []
35 | components.path = "me/library"
36 |
37 | for (key, value) in types {
38 | let values = value.map { $0.rawValue }.joined(separator: ",")
39 | queryItems.append(URLQueryItem(name: key.type, value: values))
40 | }
41 |
42 | components.queryItems = queryItems
43 |
44 | guard let url = components.url else {
45 | throw URLError(.badURL)
46 | }
47 | return url
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Multiple Resources/MusicLibraryResources/MusicLibraryResourcesResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicLibraryResourcesResponse.swift
3 | //
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// An object that contains results for a catalog resources request.
9 | public struct MusicLibraryResourcesResponse {
10 | /// A collection of playlists.
11 | public var playlists: Playlists {
12 | MusicItemCollection(items.compactMap { item in
13 | guard case let .playlist(playlist) = item else { return nil }
14 | return playlist
15 | })
16 | }
17 |
18 | /// A collection of artists.
19 | public var artists: Artists {
20 | MusicItemCollection(items.compactMap { item in
21 | guard case let .artist(artist) = item else { return nil }
22 | return artist
23 | })
24 | }
25 |
26 | /// A collection of albums.
27 | public var albums: Albums {
28 | MusicItemCollection(items.compactMap { item in
29 | guard case let .album(album) = item else { return nil }
30 | return album
31 | })
32 | }
33 |
34 | /// A collection of music videos.
35 | public var musicVideos: MusicVideos {
36 | MusicItemCollection(items.compactMap { item in
37 | guard case let .musicVideo(musicVideo) = item else { return nil }
38 | return musicVideo
39 | })
40 | }
41 |
42 | /// A collection of songs.
43 | public var songs: Songs {
44 | MusicItemCollection(items.compactMap { item in
45 | guard case let .song(song) = item else { return nil }
46 | return song
47 | })
48 | }
49 |
50 | /// A collection of different music items.
51 | var items: MusicLibraryResourcesTypes
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Multiple Resources/MusicLibraryResources/MusicLibraryResourcesType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicLibraryResourcesType.swift
3 | //
4 | //
5 | // Created by Rudrank Riyam on 23/04/22.
6 | //
7 |
8 | /// A collection of music catalog resources types.
9 | typealias MusicLibraryResourcesTypes = MusicItemCollection
10 |
11 | /// A generic music item to represent each of the library music items.
12 | public enum MusicLibraryResourcesType {
13 | case album(Album)
14 | case song(Song)
15 | case playlist(Playlist)
16 | case artist(Artist)
17 | case musicVideo(MusicVideo)
18 |
19 | public enum Key: String, Codable {
20 | case songs = "library-songs"
21 | case albums = "library-albums"
22 | case playlists = "library-playlists"
23 | case artists = "library-artists"
24 | case musicVideos = "library-music-videos"
25 |
26 | public var type: String {
27 | "ids[\(rawValue)]".removingPercentEncoding!
28 | }
29 | }
30 | }
31 |
32 | extension MusicLibraryResourcesType: MusicItem {
33 | public var id: MusicItemID {
34 | let id: MusicItemID
35 |
36 | switch self {
37 | case let .song(song): id = song.id
38 | case let .playlist(playlist): id = playlist.id
39 | case let .artist(artist): id = artist.id
40 | case let .album(album): id = album.id
41 | case let .musicVideo(musicVideo): id = musicVideo.id
42 | }
43 |
44 | return id
45 | }
46 | }
47 |
48 | extension MusicLibraryResourcesType: Decodable {
49 | enum CodingKeys: CodingKey {
50 | case type
51 | }
52 |
53 | public init(from decoder: Decoder) throws {
54 | let values = try decoder.container(keyedBy: CodingKeys.self)
55 | let type = try values.decode(MusicLibraryResourcesType.Key.self, forKey: .type)
56 |
57 | switch type {
58 | case .songs:
59 | let song = try Song(from: decoder)
60 | self = .song(song)
61 | case .playlists:
62 | let playlist = try Playlist(from: decoder)
63 | self = .playlist(playlist)
64 | case .musicVideos:
65 | let musicVideo = try MusicVideo(from: decoder)
66 | self = .musicVideo(musicVideo)
67 | case .albums:
68 | let album = try Album(from: decoder)
69 | self = .album(album)
70 | case .artists:
71 | let artist = try Artist(from: decoder)
72 | self = .artist(artist)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/AlbumProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlbumProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of an album.
9 | public typealias AlbumProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of an album.
12 | public typealias AlbumProperties = [AlbumProperty]
13 |
14 | extension AlbumProperties {
15 | /// All the album properties like artist URL, genres, artists, appears on,
16 | /// other versions, record labels, related albums, related videos, and tracks.
17 | /// For iOS 16+, adds the audio variants property.
18 | public static var all: Self {
19 | var properties: Self = [.artistURL, .genres, .artists, .appearsOn, .otherVersions, .recordLabels, .relatedAlbums, .relatedVideos, .tracks]
20 |
21 | if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) {
22 | properties += [.audioVariants]
23 | return properties
24 | } else {
25 | return properties
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/ArtistProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArtistProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of an artist.
9 | public typealias ArtistProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of an artist.
12 | public typealias ArtistProperties = [ArtistProperty]
13 |
14 | extension ArtistProperties {
15 | public static var all: Self {
16 | [.genres, .station, .musicVideos, .albums, .playlists, .appearsOnAlbums, .fullAlbums, .featuredAlbums, .liveAlbums, .compilationAlbums, .featuredPlaylists, .latestRelease, .topSongs, .topMusicVideos, .similarArtists, .singles]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/CuratorProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CuratorProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of an artist.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public typealias CuratorProperty = PartialMusicAsyncProperty
11 |
12 | /// Additional properties/relationships of an artist.
13 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
14 | public typealias CuratorProperties = [CuratorProperty]
15 |
16 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
17 | extension CuratorProperties {
18 | public static var all: Self {
19 | [.playlists]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/MusicVideoProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicVideoProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of a music video.
9 | public typealias MusicVideoProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of a music video.
12 | public typealias MusicVideoProperties = [MusicVideoProperty]
13 |
14 | extension MusicVideoProperties {
15 | public static var all: Self {
16 | [.albums, .genres, .artists, .artistURL, .moreInGenre, .songs, .moreByArtist]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/PlaylistProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaylistProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of a playlist.
9 | public typealias PlaylistProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of a playlist.
12 | public typealias PlaylistProperties = [PlaylistProperty]
13 |
14 | extension PlaylistProperties {
15 | public static var all: Self {
16 | var properties: Self = [.tracks, .featuredArtists, .moreByCurator]
17 |
18 | if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) {
19 | properties += [.curator, .radioShow]
20 | return properties
21 | } else {
22 | return properties
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/RadioShowProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RadioShowProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of a radio show.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public typealias RadioShowProperty = PartialMusicAsyncProperty
11 |
12 | /// Additional properties/relationships of a radio show.
13 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
14 | public typealias RadioShowProperties = [RadioShowProperty]
15 |
16 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
17 | extension RadioShowProperties {
18 | public static var all: Self {
19 | [.playlists]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/RecordLabelProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordLabelProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of a record label.
9 | public typealias RecordLabelProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of a record label.
12 | public typealias RecordLabelProperties = [RecordLabelProperty]
13 |
14 | extension RecordLabelProperties {
15 | public static var all: Self {
16 | [.latestReleases, .topReleases]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Item Properties/SongProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongProperties.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// Additional property/relationship of a song.
9 | public typealias SongProperty = PartialMusicAsyncProperty
10 |
11 | /// Additional properties/relationships of a song.
12 | public typealias SongProperties = [SongProperty]
13 |
14 | extension SongProperties {
15 | public static var all: Self {
16 | var properties: Self = [.albums, .artists, .composers, .genres, .musicVideos, .artistURL, .station]
17 |
18 | if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *) {
19 | properties += [.audioVariants]
20 | return properties
21 | } else {
22 | return properties
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Albums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Albums.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of albums.
9 | public typealias Albums = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Artists.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Artists.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of artists.
9 | public typealias Artists = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Curators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Curators.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of curators.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public typealias Curators = MusicItemCollection
11 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Genres.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Genres.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of genres.
9 | public typealias Genres = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/LibraryPlaylists.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryPlaylists.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 24/12/22.
6 | //
7 |
8 | /// A collection of library playlists.
9 | public typealias LibraryPlaylists = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/MusicVideos.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MusicVideos.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of music videos.
9 | public typealias MusicVideos = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Playlists.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Playlists.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of playlists.
9 | public typealias Playlists = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/RadioShows.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RadioShows.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of radio shows.
9 | @available(iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 9.0, visionOS 1.0, *)
10 | public typealias RadioShows = MusicItemCollection
11 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/RecordLabels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordLabels.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of record labels.
9 | public typealias RecordLabels = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Songs.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Songs.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of songs.
9 | public typealias Songs = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/StationGenres.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StationGenre.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 12/03/23.
6 | //
7 |
8 | /// A collection of station genres.
9 | public typealias StationGenres = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Stations.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Stations.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 22/12/22.
6 | //
7 |
8 | /// A collection of stations.
9 | public typealias Stations = MusicItemCollection
10 |
--------------------------------------------------------------------------------
/Sources/MusadoraKit/Music Items/Tracks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tracks.swift
3 | // MusadoraKit
4 | //
5 | // Created by Rudrank Riyam on 26/12/22.
6 | //
7 |
8 | /// A typealias representing a collection of `Track` items.
9 | ///
10 | /// This type allows you to work with a group of `Track` objects in the context of a music collection.
11 | ///
12 | /// Example usage:
13 | ///
14 | /// let myTracks: Tracks = fetchTracks()
15 | /// print(myTracks.count)
16 | ///
17 | /// **Note:** This is built on top of `MusicItemCollection`, which provides a generic mechanism to work with music items.
18 | public typealias Tracks = MusicItemCollection