Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slight refactor to better accommodate exporting any of your Spotify playlists #146

Merged
merged 5 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: Manually trigger the action
name: Save seasonal playlist

on:
workflow_dispatch:
inputs:
playlistName:
type: string
description: The name of the Spotify playlist.
schedule:
- cron: "00 01 20 Mar,Jun,Sep,Dec *"

jobs:
spotify-to-yaml:
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/spotify.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: Save Spotify playlist

on:
schedule:
- cron: "00 01 20 Mar,Jun,Sep,Dec *"
workflow_dispatch:
inputs:
playlist-name:
description: Your Spotify playlist name that you want to export. Required for non-seasonal playlist export.
required: true
type: string

jobs:
spotify-to-yaml:
Expand Down
56 changes: 39 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# spotify-to-yaml-action

Export a seasonal Spotify playlist to YAML.
Export a Spotify playlist to YAML.

At the end of each season, the workflow will fetch last season's playlists, add the the contents to `_data/playlist.yml` and save the playlist thumbnail image to the repository.
This workflow can:

- Export your Spotify playlist to yaml.
- Fetch last season's playlists, add the the contents to `_data/playlist.yml` and save the playlist thumbnail image to the repository.

## Set up

This workflow requires that you name your Spotify playlists using the following format: `YYYY {season}`. If you use different names for the seasons, you can use the `season-names` [action input](#action-options) to reflect that. Examples:
To connect your Spotify account to this workflow, set the following [secrets to your repository](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository). You can find these values from the [Spotify API dashboard](https://developer.spotify.com/dashboard):

- `SpotifyClientID`
- `SpotifyClientSecret`

## Seasonal set up

To take part in season playlist export, you will need to name your Spotify playlists with the following pattern: `YYYY {season}`. If you use different names for the seasons, you can use the `season-names` [action input](#action-options) to reflect that. Examples:

- `2021 Fall`
- `2021/2022 Winter`
- `2022 Spring`
- `2022 Summer`

You must also set the following [secrets to your repository](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) to connect to Spotify. You can find these values from the [Spotify API dashboard](https://developer.spotify.com/dashboard):

- `SpotifyClientID`
- `SpotifyClientSecret`

<!-- START GENERATED DOCUMENTATION -->

## Set up the workflow
Expand All @@ -26,9 +31,14 @@ To use this action, create a new workflow in `.github/workflows` and modify it a

```yml
name: Save Spotify playlist

on:
schedule:
- cron: "00 01 20 Mar,Jun,Sep,Dec *"
workflow_dispatch:
inputs:
playlist-name:
description: Your Spotify playlist name that you want to export. Required for non-seasonal playlist export.
required: true
type: string

jobs:
spotify-to-yaml:
Expand Down Expand Up @@ -59,16 +69,14 @@ jobs:
### Additional example workflows

<details>
<summary>Manually trigger the action</summary>
<summary>Save seasonal playlist</summary>

```yml
name: Manually trigger the action
name: Save seasonal playlist

on:
workflow_dispatch:
inputs:
playlistName:
type: string
description: The name of the Spotify playlist.
schedule:
- cron: "00 01 20 Mar,Jun,Sep,Dec *"

jobs:
spotify-to-yaml:
Expand Down Expand Up @@ -105,4 +113,18 @@ jobs:
- `filename`: The YAML file to write your playlists. Default: `_data/playlists.yml`.

- `season-names`: The season names in order by the season that ends in March, June, September, and then December. Default: `Winter,Spring,Summer,Fall`.

## Trigger the action

To trigger the action, [create a workflow dispatch event](https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event) with the following body parameters:

```js
{
"ref": "main", // Required. The git reference for the workflow, a branch or tag name.
"inputs": {
"playlist-name": "", // Required. Your Spotify playlist name that you want to export. Required for non-seasonal playlist export.
}
}
```

<!-- END GENERATED DOCUMENTATION -->
204 changes: 89 additions & 115 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49298,49 +49298,36 @@ var jsYaml = {


;// CONCATENATED MODULE: ./src/write-file.ts
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};


function updateMain(data, filename) {
return __awaiter(this, void 0, void 0, function* () {
try {
const newContents = yield buildNewMain(data, filename);
return yield (0,promises_namespaceObject.writeFile)(filename, newContents);
}
catch (error) {
throw new Error(error);
}
});
async function updateMain(data, filename) {
try {
const newContents = await buildNewMain(data, filename);
return await (0,promises_namespaceObject.writeFile)(filename, newContents);
}
catch (error) {
throw new Error(error);
}
}
function buildNewMain(data, filename) {
return __awaiter(this, void 0, void 0, function* () {
try {
const currentPlaylists = (yield (0,promises_namespaceObject.readFile)(filename, "utf-8")) || "";
const currentJson = load(currentPlaylists);
const newPlaylist = {
playlist: data.name,
spotify: data.url,
tracks: data.tracks.map(({ name, artist, album }) => ({
track: name,
artist,
album,
})),
};
const json = [...(currentPlaylists && [...currentJson]), newPlaylist];
return dump(json);
}
catch (error) {
throw new Error(error);
}
});
async function buildNewMain(data, filename) {
try {
const currentPlaylists = (await (0,promises_namespaceObject.readFile)(filename, "utf-8")) || "";
const currentJson = load(currentPlaylists);
const newPlaylist = {
playlist: data.name,
spotify: data.url,
tracks: data.tracks.map(({ name, artist, album }) => ({
track: name,
artist,
album,
})),
};
const json = [...(currentPlaylists && [...currentJson]), newPlaylist];
return dump(json);
}
catch (error) {
throw new Error(error);
}
}

// EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js
Expand All @@ -49351,33 +49338,28 @@ var github = __nccwpck_require__(5438);


function learnPlaylistName() {
let playlistName;
const payload = github.context.payload.inputs;
if (payload && payload.playlistName) {
playlistName = payload.playlistName;
}
if (!playlistName) {
const today = new Date();
const month = process.env.MONTH
? parseInt(process.env.MONTH)
: today.getMonth();
const year = process.env.YEAR
? parseInt(process.env.YEAR)
: today.getFullYear();
const [marchEnd, juneEnd, septemberEnd, decemberEnd] = validateSeasonNames();
const seasons = {
2: marchEnd,
5: juneEnd,
8: septemberEnd,
11: decemberEnd,
};
const season = seasons[month];
if (!season)
throw new Error(`The current month does not match an end of season month.`);
playlistName = `${month === 2 ? `${year - 1}/${year}` : year} ${season}`;
}
(0,lib_core.exportVariable)("playlist", playlistName);
return playlistName;
if (payload && payload["playlist-name"]) {
return payload["playlist-name"];
}
const today = new Date();
const month = process.env.MONTH
? parseInt(process.env.MONTH)
: today.getMonth();
const year = process.env.YEAR
? parseInt(process.env.YEAR)
: today.getFullYear();
const [marchEnd, juneEnd, septemberEnd, decemberEnd] = validateSeasonNames();
const seasons = {
2: marchEnd,
5: juneEnd,
8: septemberEnd,
11: decemberEnd,
};
const season = seasons[month];
if (!season)
throw new Error(`The current month does not match an end of season month.`);
return `${month === 2 ? `${year - 1}/${year}` : year} ${season}`;
}
function validateSeasonNames() {
const seasonNames = (0,lib_core.getInput)("season-names")
Expand All @@ -49392,88 +49374,80 @@ function validateSeasonNames() {
var server = __nccwpck_require__(5337);
var server_default = /*#__PURE__*/__nccwpck_require__.n(server);
;// CONCATENATED MODULE: ./src/list-playlists.ts
var list_playlists_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};


function listPlaylists(listName) {
return list_playlists_awaiter(this, void 0, void 0, function* () {
async function listPlaylists(listName) {
try {
const spotifyApi = new (server_default())({
clientId: process.env.SpotifyClientID,
clientSecret: process.env.SpotifyClientSecret,
});
const username = (0,lib_core.getInput)("spotify-username");
const { body: { access_token }, } = yield spotifyApi.clientCredentialsGrant();
const { body: { access_token }, } = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(access_token);
const { body } = yield spotifyApi.getUserPlaylists(username);
const { body } = await spotifyApi.getUserPlaylists(username);
const findPlaylist = body.items.find(({ name }) => name === listName);
if (!findPlaylist) {
(0,lib_core.setFailed)(`Could not find playlist "${listName}". Is it private?`);
return;
throw new Error(`Could not find playlist "${listName}". Is it private?`);
}
const { body: { items }, } = yield spotifyApi.getPlaylistTracks(findPlaylist.id);
const { body: { items }, } = await spotifyApi.getPlaylistTracks(findPlaylist.id);
if (!items.length)
throw new Error("Playlist has no tracks.");
return formatTracks({
name: findPlaylist.name,
external_urls: findPlaylist.external_urls,
images: findPlaylist.images,
tracks: items,
});
});
}
catch (error) {
throw new Error(error);
}
}
function formatTracks({ name, external_urls, images, tracks, }) {
const largestImage = images.sort((a, b) => b.width - a.width)[0];
const largestImage = images.sort((a, b) => (b.width || 0) - (a.width || 0))[0];
return {
name,
formatted_name: name.replace("/", "-").toLowerCase().replace(" ", "-"),
formatted_name: formatName(name),
url: external_urls.spotify,
tracks: tracks.map(({ track }) => ({
name: track.name,
artist: track.artists.map(({ name }) => name).join(", "),
album: track.album.name,
name: track?.name,
artist: track?.artists.map(({ name }) => name).join(", "),
album: track?.album.name,
})),
image: largestImage.url,
};
}
function formatName(name) {
return name
.replace(/\s/g, "-")
.replace(/[^a-zA-Z0-9-]/g, "")
.replace(/-+/g, "-")
.toLowerCase();
}

;// CONCATENATED MODULE: ./src/index.ts
var src_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};




function action() {
return src_awaiter(this, void 0, void 0, function* () {
try {
const filename = (0,lib_core.getInput)("filename");
const playlistName = learnPlaylistName();
const playlist = (yield listPlaylists(playlistName));
// export image variable to be downloaded latter
(0,lib_core.exportVariable)("PlaylistImageOutput", `${playlist.formatted_name}.png`);
(0,lib_core.exportVariable)("PlaylistImage", playlist.image);
// replace Spotify image url with local version
playlist.image = `${playlist.formatted_name}.png`;
// save tracks to playlists.yml
yield updateMain(playlist, filename);
}
catch (error) {
(0,lib_core.setFailed)(error.message);
}
});
async function action() {
try {
const filename = (0,lib_core.getInput)("filename");
const playlistName = learnPlaylistName();
const playlist = await listPlaylists(playlistName);
// export image variable to be downloaded latter
(0,lib_core.exportVariable)("playlist", playlistName);
(0,lib_core.exportVariable)("PlaylistImageOutput", `${playlist.formatted_name}.png`);
(0,lib_core.exportVariable)("PlaylistImage", playlist.image);
// replace Spotify image url with local version
playlist.image = `${playlist.formatted_name}.png`;
// save tracks to playlists.yml
await updateMain(playlist, filename);
}
catch (error) {
(0,lib_core.setFailed)(error.message);
}
}
/* harmony default export */ const src = (action());

Expand Down
Loading