mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-03-05 20:49:04 -08:00
parent
7071c4d74c
commit
870af53d35
41
docs/docs/segment-ytm.md
Normal file
41
docs/docs/segment-ytm.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
id: ytm
|
||||||
|
title: YouTube Music
|
||||||
|
sidebar_label: YouTube Music
|
||||||
|
---
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
Shows the currently playing song in the [YouTube Music Desktop App](https://github.com/ytmdesktop/ytmdesktop).
|
||||||
|
|
||||||
|
**NOTE**: You **must** enable Remote Control in YTMDA for this segment to work: `Settings > Integrations > Remote Control`
|
||||||
|
|
||||||
|
It is fine if `Protect remote control with password` is automatically enabled. This segment does not require the
|
||||||
|
Remote Control password.
|
||||||
|
|
||||||
|
## Sample Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ytm",
|
||||||
|
"style": "powerline",
|
||||||
|
"powerline_symbol": "\uE0B0",
|
||||||
|
"foreground": "#ffffff",
|
||||||
|
"background": "#FF0000",
|
||||||
|
"properties": {
|
||||||
|
"prefix": "\uF16A ",
|
||||||
|
"playing_icon": "\uE602 ",
|
||||||
|
"paused_icon": "\uF8E3 ",
|
||||||
|
"stopped_icon": "\uF04D ",
|
||||||
|
"track_separator" : " - "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
- playing_icon: `string` - text/icon to show when playing - defaults to `\uE602 `
|
||||||
|
- paused_icon: `string` - text/icon to show when paused - defaults to `\uF8E3 `
|
||||||
|
- stopped_icon: `string` - text/icon to show when paused - defaults to `\uF04D `
|
||||||
|
- track_separator: `string` - text/icon to put between the artist and song name - defaults to ` - `
|
||||||
|
- api_url: `string` - the YTMDA Remote Control API URL- defaults to `http://localhost:9863`
|
|
@ -35,6 +35,7 @@ module.exports = {
|
||||||
"terraform",
|
"terraform",
|
||||||
"text",
|
"text",
|
||||||
"time",
|
"time",
|
||||||
|
"ytm",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,9 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -43,6 +45,7 @@ type environmentInfo interface {
|
||||||
getBatteryInfo() (*battery.Battery, error)
|
getBatteryInfo() (*battery.Battery, error)
|
||||||
getShellName() string
|
getShellName() string
|
||||||
getWindowTitle(imageName, windowTitleRegex string) (string, error)
|
getWindowTitle(imageName, windowTitleRegex string) (string, error)
|
||||||
|
doGet(url string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type environment struct {
|
type environment struct {
|
||||||
|
@ -213,6 +216,23 @@ func (env *environment) getShellName() string {
|
||||||
return strings.Trim(shell, " ")
|
return strings.Trim(shell, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (env *environment) doGet(url string) ([]byte, error) {
|
||||||
|
request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
func cleanHostName(hostName string) string {
|
func cleanHostName(hostName string) string {
|
||||||
garbage := []string{
|
garbage := []string{
|
||||||
".lan",
|
".lan",
|
||||||
|
|
15
httpclient.go
Normal file
15
httpclient.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inspired by: https://www.thegreatcodeadventure.com/mocking-http-requests-in-golang/
|
||||||
|
|
||||||
|
type httpClient interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
client httpClient = &http.Client{}
|
||||||
|
)
|
|
@ -85,6 +85,8 @@ const (
|
||||||
Plain SegmentStyle = "plain"
|
Plain SegmentStyle = "plain"
|
||||||
// Diamond writes the prompt shaped with a leading and trailing symbol
|
// Diamond writes the prompt shaped with a leading and trailing symbol
|
||||||
Diamond SegmentStyle = "diamond"
|
Diamond SegmentStyle = "diamond"
|
||||||
|
// YTM writes YouTube Music information and status
|
||||||
|
YTM SegmentType = "ytm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (segment *Segment) string() string {
|
func (segment *Segment) string() string {
|
||||||
|
@ -139,6 +141,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
|
||||||
Terraform: &terraform{},
|
Terraform: &terraform{},
|
||||||
Golang: &golang{},
|
Golang: &golang{},
|
||||||
Julia: &julia{},
|
Julia: &julia{},
|
||||||
|
YTM: &ytm{},
|
||||||
}
|
}
|
||||||
if writer, ok := functions[segment.Type]; ok {
|
if writer, ok := functions[segment.Type]; ok {
|
||||||
props := &properties{
|
props := &properties{
|
||||||
|
|
|
@ -113,6 +113,11 @@ func (env *MockedEnvironment) getWindowTitle(imageName, windowTitleRegex string)
|
||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (env *MockedEnvironment) doGet(url string) ([]byte, error) {
|
||||||
|
args := env.Called(url)
|
||||||
|
return args.Get(0).([]byte), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
homeGates = "/home/gates"
|
homeGates = "/home/gates"
|
||||||
homeBill = "/home/bill"
|
homeBill = "/home/bill"
|
||||||
|
|
106
segment_ytm.go
Normal file
106
segment_ytm.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ytm struct {
|
||||||
|
props *properties
|
||||||
|
env environmentInfo
|
||||||
|
status playStatus
|
||||||
|
artist string
|
||||||
|
track string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// APIURL is the YTMDA Remote Control API URL property.
|
||||||
|
APIURL Property = "api_url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (y *ytm) string() string {
|
||||||
|
icon := ""
|
||||||
|
separator := y.props.getString(TrackSeparator, " - ")
|
||||||
|
switch y.status {
|
||||||
|
case paused:
|
||||||
|
icon = y.props.getString(PausedIcon, "\uF8E3 ")
|
||||||
|
case playing:
|
||||||
|
icon = y.props.getString(PlayingIcon, "\uE602 ")
|
||||||
|
case stopped:
|
||||||
|
return y.props.getString(StoppedIcon, "\uF04D ")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s%s%s", icon, y.artist, separator, y.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *ytm) enabled() bool {
|
||||||
|
err := y.setStatus()
|
||||||
|
// If we don't get a response back (error), the user isn't running
|
||||||
|
// YTMDA, or they don't have the RC API enabled.
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *ytm) init(props *properties, env environmentInfo) {
|
||||||
|
y.props = props
|
||||||
|
y.env = env
|
||||||
|
}
|
||||||
|
|
||||||
|
type playStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
playing playStatus = iota
|
||||||
|
paused
|
||||||
|
stopped
|
||||||
|
)
|
||||||
|
|
||||||
|
type ytmdaStatusResponse struct {
|
||||||
|
player `json:"player"`
|
||||||
|
track `json:"track"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type player struct {
|
||||||
|
HasSong bool `json:"hasSong"`
|
||||||
|
IsPaused bool `json:"isPaused"`
|
||||||
|
VolumePercent int `json:"volumePercent"`
|
||||||
|
SeekbarCurrentPosition int `json:"seekbarCurrentPosition"`
|
||||||
|
SeekbarCurrentPositionHuman string `json:"seekbarCurrentPositionHuman"`
|
||||||
|
StatePercent float64 `json:"statePercent"`
|
||||||
|
LikeStatus string `json:"likeStatus"`
|
||||||
|
RepeatType string `json:"repeatType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type track struct {
|
||||||
|
Author string `json:"author"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Album string `json:"album"`
|
||||||
|
Cover string `json:"cover"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
DurationHuman string `json:"durationHuman"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
IsVideo bool `json:"isVideo"`
|
||||||
|
IsAdvertisement bool `json:"isAdvertisement"`
|
||||||
|
InLibrary bool `json:"inLibrary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *ytm) setStatus() error {
|
||||||
|
// https://github.com/ytmdesktop/ytmdesktop/wiki/Remote-Control-API
|
||||||
|
url := y.props.getString(APIURL, "http://localhost:9863")
|
||||||
|
body, err := y.env.doGet(url + "/query")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := new(ytmdaStatusResponse)
|
||||||
|
err = json.Unmarshal(body, &q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
y.status = playing
|
||||||
|
if !q.player.HasSong {
|
||||||
|
y.status = stopped
|
||||||
|
} else if q.player.IsPaused {
|
||||||
|
y.status = paused
|
||||||
|
}
|
||||||
|
y.artist = q.track.Author
|
||||||
|
y.track = q.track.Title
|
||||||
|
return nil
|
||||||
|
}
|
91
segment_ytm_test.go
Normal file
91
segment_ytm_test.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestYTMStringPlayingSong(t *testing.T) {
|
||||||
|
expected := "\ue602 Candlemass - Spellbreaker"
|
||||||
|
y := &ytm{
|
||||||
|
artist: "Candlemass",
|
||||||
|
track: "Spellbreaker",
|
||||||
|
status: playing,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, y.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMStringPausedSong(t *testing.T) {
|
||||||
|
expected := "\uF8E3 Candlemass - Spellbreaker"
|
||||||
|
y := &ytm{
|
||||||
|
artist: "Candlemass",
|
||||||
|
track: "Spellbreaker",
|
||||||
|
status: paused,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, y.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMStringStoppedSong(t *testing.T) {
|
||||||
|
expected := "\uf04d "
|
||||||
|
y := &ytm{
|
||||||
|
artist: "Candlemass",
|
||||||
|
track: "Spellbreaker",
|
||||||
|
status: stopped,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, y.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
func bootstrapYTMDATest(json string, err error) *ytm {
|
||||||
|
url := "http://localhost:1337"
|
||||||
|
env := new(MockedEnvironment)
|
||||||
|
env.On("doGet", url+"/query").Return([]byte(json), err)
|
||||||
|
props := &properties{
|
||||||
|
values: map[Property]interface{}{
|
||||||
|
APIURL: url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ytm := &ytm{
|
||||||
|
env: env,
|
||||||
|
props: props,
|
||||||
|
}
|
||||||
|
return ytm
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMDAPlaying(t *testing.T) {
|
||||||
|
json := `{ "player": { "hasSong": true, "isPaused": false }, "track": { "author": "Candlemass", "title": "Spellbreaker" } }`
|
||||||
|
ytm := bootstrapYTMDATest(json, nil)
|
||||||
|
err := ytm.setStatus()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, playing, ytm.status)
|
||||||
|
assert.Equal(t, "Candlemass", ytm.artist)
|
||||||
|
assert.Equal(t, "Spellbreaker", ytm.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMDAPaused(t *testing.T) {
|
||||||
|
json := `{ "player": { "hasSong": true, "isPaused": true }, "track": { "author": "Candlemass", "title": "Spellbreaker" } }`
|
||||||
|
ytm := bootstrapYTMDATest(json, nil)
|
||||||
|
err := ytm.setStatus()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, paused, ytm.status)
|
||||||
|
assert.Equal(t, "Candlemass", ytm.artist)
|
||||||
|
assert.Equal(t, "Spellbreaker", ytm.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMDAStopped(t *testing.T) {
|
||||||
|
json := `{ "player": { "hasSong": false }, "track": { "author": "", "title": "" } }`
|
||||||
|
ytm := bootstrapYTMDATest(json, nil)
|
||||||
|
err := ytm.setStatus()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, stopped, ytm.status)
|
||||||
|
assert.Equal(t, "", ytm.artist)
|
||||||
|
assert.Equal(t, "", ytm.track)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYTMDAError(t *testing.T) {
|
||||||
|
json := `{ "player": { "hasSong": false }, "track": { "author": "", "title": "" } }`
|
||||||
|
ytm := bootstrapYTMDATest(json, errors.New("Oh noes"))
|
||||||
|
enabled := ytm.enabled()
|
||||||
|
assert.False(t, enabled)
|
||||||
|
}
|
|
@ -103,7 +103,8 @@
|
||||||
"dotnet",
|
"dotnet",
|
||||||
"terraform",
|
"terraform",
|
||||||
"go",
|
"go",
|
||||||
"julia"
|
"julia",
|
||||||
|
"ytm"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
|
@ -1065,6 +1066,53 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "ytm" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"title": "YouTube Music Desktop App Segment",
|
||||||
|
"description": "https://ohmyposh.dev/docs/ytm",
|
||||||
|
"properties": {
|
||||||
|
"properties": {
|
||||||
|
"properties": {
|
||||||
|
"playing_icon": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "User Info Separator",
|
||||||
|
"description": "Text/icon to show when playing",
|
||||||
|
"default": "\uE602"
|
||||||
|
},
|
||||||
|
"paused_icon": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "SSH Icon",
|
||||||
|
"description": "Text/icon to show when paused",
|
||||||
|
"default": "\uF8E3"
|
||||||
|
},
|
||||||
|
"stopped_icon": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "SSH Icon",
|
||||||
|
"description": "Text/icon to show when stopped",
|
||||||
|
"default": "\uF04D"
|
||||||
|
},
|
||||||
|
"track_separator": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "SSH Icon",
|
||||||
|
"description": "Text/icon to put between the artist and song name",
|
||||||
|
"default": " - "
|
||||||
|
},
|
||||||
|
"api_url": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "API URL",
|
||||||
|
"description": "The YTMDA Remote Control API URL",
|
||||||
|
"default": "http://localhost:9863"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue