├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── activity.go
├── cycler.go
├── discord-dev-assets
├── heart.png
└── lfm_logo.png
├── github-assets
├── screenshot-1.png
└── screenshot-2.png
├── go.mod
├── go.sum
├── main.go
└── rpc.go
/.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 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Desktop (please complete the following information):**
28 |
29 | - OS: [e.g. Windows, Linux, Mac]
30 | - Version [e.g. 22]
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/.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/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 |
12 | - name: Set up Go
13 | uses: actions/setup-go@v3
14 | with:
15 | go-version: 1.18
16 |
17 | - name: Build
18 | run: |
19 | GOOS=darwin GOARCH=amd64 go build -o "lfm-cli-macos-amd64.${{ github.ref_name }}" -v ./...
20 | GOOS=darwin GOARCH=arm64 go build -o "lfm-cli-macos-arm64.${{ github.ref_name }}" -v ./...
21 | GOOS=windows GOARCH=amd64 go build -o "lfm-cli-windows-amd64.${{ github.ref_name }}.exe" -v ./...
22 | GOOS=linux GOARCH=amd64 go build -o "lfm-cli-linux-amd64.${{ github.ref_name }}" -v ./...
23 |
24 | - name: Upload artifact
25 | uses: actions/upload-artifact@v2
26 | with:
27 | name: builds
28 | path: lfm-cli-*
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bin
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | jamesding365@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 James Ding
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | lfm-cli
7 |
8 |
9 |
10 | Show your fellow gamers and friends what you're listening to on Last.FM without touching a single API Key!
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Sample Images
23 |
24 |
25 | 
26 |
27 |
28 |
29 | # Usage
30 |
31 | lfm-cli works right out of the box - no configuration needed.
32 |
33 | To get started, download the latest [release](https://github.com/lfm2discord/lfm2discord-cli/releases). These binaries
34 | are built on GitHub Actions.
35 |
36 | **With [Discord](https://discord.com/) open**, run the following binary in your console
37 |
38 | ```console
39 | foo@bar:~$ lfm-cli -u MYUSERNAME
40 | ```
41 |
42 | For full reference on flags, run the binary with the `-h` or `--help` flag.
43 |
44 | ## Known Issues
45 |
46 | - Requires restart when Discord is closed while the application is active.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/activity.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/hugolgst/rich-go/client"
6 | lfm "github.com/twangodev/lfm-api"
7 | "golang.org/x/net/html"
8 | )
9 |
10 | func createActivity(s lfm.Scrobble, songLink bool) client.Activity {
11 |
12 | var songButton *client.Button
13 | // Determines whether to display profile link in buttons
14 | if songLink {
15 | dataLinkTitle := s.DataLinkTitle
16 | dataLink := s.DataLink
17 | if dataLinkTitle == "" {
18 | dataLinkTitle = "View scrobble on Last.fm"
19 | dataLink = fmt.Sprintf("%vmusic/%v/%v", lfm.LastFmUrl, html.EscapeString(s.Artist), html.EscapeString(s.Name))
20 | }
21 | songButton = &client.Button{Label: dataLinkTitle, Url: dataLink}
22 | }
23 |
24 | var buttons []*client.Button
25 | if songButton != nil {
26 | buttons = []*client.Button{songButton}
27 | }
28 | if showProfile {
29 | buttons = []*client.Button{{Label: "Visit last.fm Profile", Url: profileUrl}}
30 | }
31 |
32 | if showProfile && songButton != nil {
33 | buttons = []*client.Button{{Label: "Visit last.fm Profile", Url: profileUrl}, songButton}
34 | }
35 |
36 | // Determines whether to display the heart for the smallImage
37 | smallUrl := "lfm_logo"
38 | if showLoved && s.Loved { // Change small image to heart if user enable loved and scrobble is loved
39 | smallUrl = "heart"
40 | }
41 |
42 | var coverUrl string
43 | if covers {
44 | coverUrl = s.CoverArtUrl
45 | }
46 |
47 | var timestamps *client.Timestamps
48 | if elapsed {
49 | timestamps = &client.Timestamps{
50 | Start: &s.DataTimestamp,
51 | }
52 | }
53 |
54 | return client.Activity{
55 | Details: s.Name,
56 | State: fmt.Sprintf("by %v", s.Artist),
57 | LargeImage: coverUrl,
58 | LargeText: s.Album,
59 | SmallImage: smallUrl,
60 | SmallText: info,
61 | Timestamps: timestamps,
62 | Buttons: buttons,
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/cycler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/hugolgst/rich-go/client"
6 | log "github.com/sirupsen/logrus"
7 | lfm "github.com/twangodev/lfm-api"
8 | "time"
9 | )
10 |
11 | var info = fmt.Sprintf("%v • %v", name, version)
12 | var ts = time.Now()
13 |
14 | func cycle() {
15 | s, _ := lfm.GetActiveScrobble(username) // Fetch latest scrobble, emptyScrobble if no new scrobble
16 |
17 | if keepStatus {
18 | login()
19 | if !s.Active {
20 | err := client.SetActivity(client.Activity{
21 | Details: name,
22 | State: version,
23 | LargeImage: "lfm_logo",
24 | })
25 | if err != nil {
26 | log.Warnln("Failed to keep activity.")
27 | return
28 | }
29 | }
30 | } else {
31 | // Login logout logic
32 | if s.Active { // Login if scrobble detected and if currently logged out
33 | if !loggedIn {
34 | log.Info("New scrobble detected. Logging in.")
35 | login()
36 | }
37 | } else { // No new scrobble
38 | if loggedIn { // Logout if logged in
39 | log.Info("No scrobble detected. Logging out.")
40 | logout()
41 | } else { // Retain logout state
42 | log.Traceln("No new scrobble detected.")
43 | }
44 | return
45 | }
46 | }
47 |
48 | if ts != s.DataTimestamp { // Update old timestamp to match current scrobble
49 | ts = s.DataTimestamp
50 | log.WithFields(log.Fields{"scrobbling": s}).Infoln("Updating presence.")
51 | } else { // Prevents update of the same scrobble, use timestamp to differentiate
52 | return
53 | }
54 |
55 | // First RPC attempt is without songLink
56 | err1 := client.SetActivity(createActivity(s, false))
57 | if err1 != nil {
58 | log.Info("Failed to set base RPC. Retrying with detailed payload.")
59 | } else {
60 | log.Traceln("Successfully set base RPC.")
61 | }
62 |
63 | // Second RPC attempt is with songLink
64 | err2 := client.SetActivity(createActivity(s, true))
65 | if err2 != nil {
66 | if err1 != nil {
67 | log.Warnln("Both attempts to set RPC failed.")
68 | } else {
69 | log.Info("Failed to set detailed RPC.")
70 | }
71 | } else {
72 | log.Traceln("Successfully set detailed RPC.")
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/discord-dev-assets/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twangodev/lfm-cli/9a4a3c442e84cc3c81c82f90d679d46f35a05301/discord-dev-assets/heart.png
--------------------------------------------------------------------------------
/discord-dev-assets/lfm_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twangodev/lfm-cli/9a4a3c442e84cc3c81c82f90d679d46f35a05301/discord-dev-assets/lfm_logo.png
--------------------------------------------------------------------------------
/github-assets/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twangodev/lfm-cli/9a4a3c442e84cc3c81c82f90d679d46f35a05301/github-assets/screenshot-1.png
--------------------------------------------------------------------------------
/github-assets/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twangodev/lfm-cli/9a4a3c442e84cc3c81c82f90d679d46f35a05301/github-assets/screenshot-2.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module lfm-cli
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2
7 | github.com/mattn/go-colorable v0.1.13
8 | github.com/sirupsen/logrus v1.9.3
9 | github.com/twangodev/lfm-api v1.1.0
10 | github.com/urfave/cli/v2 v2.27.3
11 | golang.org/x/net v0.29.0
12 | )
13 |
14 | require (
15 | github.com/bozd4g/go-http-client v1.0.2 // indirect
16 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
17 | github.com/mattn/go-isatty v0.0.20 // indirect
18 | github.com/pkg/errors v0.9.1 // indirect
19 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
20 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
21 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
22 | golang.org/x/sys v0.25.0 // indirect
23 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bozd4g/go-http-client v1.0.2 h1:qVmsNAFzwhuxQG7D+mR4Yq69R5D3cmAlWCszqj2mNfI=
2 | github.com/bozd4g/go-http-client v1.0.2/go.mod h1:oMkSTEcaxBoow1/jd1pXKLK0GIeEZTmpzEMF3bdLiqU=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
4 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2 h1:9qOViOQGFIP5ar+2NorfAIsfuADEKXtklySC0zNnYf4=
9 | github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA=
10 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
11 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
12 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
20 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
21 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
22 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
25 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
26 | github.com/twangodev/lfm-api v1.1.0 h1:xxg55iZfcYs+1rAHeiCrpSJay0G3ugrp+RFcqO97jc8=
27 | github.com/twangodev/lfm-api v1.1.0/go.mod h1:Gruio6zwGqwlPUWePWnUyO82demesmC5oOsDs6R2FRE=
28 | github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M=
29 | github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
30 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
31 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
32 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
33 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
34 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
35 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
36 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
40 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
42 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
43 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
46 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/mattn/go-colorable"
6 | log "github.com/sirupsen/logrus"
7 | lfm "github.com/twangodev/lfm-api"
8 | "github.com/urfave/cli/v2"
9 | "os"
10 | "time"
11 | )
12 |
13 | const name = "lfm-cli"
14 | const version = "v1.2.3"
15 |
16 | const discordAppId = "970003417277812736"
17 |
18 | // Flags
19 | var username string
20 | var refreshInterval int
21 | var showProfile bool
22 | var showLoved bool
23 | var covers bool
24 | var elapsed bool
25 | var keepStatus bool
26 | var debug bool
27 |
28 | var profileUrl string
29 |
30 | func exec(ctx *cli.Context) error {
31 |
32 | showProfile = !ctx.Bool("hide-profile")
33 | showLoved = ctx.Bool("show-loved")
34 | covers = !ctx.Bool("rm-covers")
35 | elapsed = !ctx.Bool("rm-time")
36 | keepStatus = ctx.Bool("keep-status")
37 | debug = ctx.Bool("debug")
38 | if debug {
39 | log.SetLevel(log.TraceLevel)
40 | } else {
41 | log.SetLevel(log.InfoLevel)
42 | }
43 |
44 | profileUrl = fmt.Sprintf("%vuser/%v", lfm.LastFmUrl, username)
45 |
46 | log.WithFields(log.Fields{
47 | "username": username,
48 | "refresh_interval": refreshInterval,
49 | "show_profile": showProfile,
50 | "show_loved": showLoved,
51 | "show_covers": covers,
52 | "show_elapsed": elapsed,
53 | "keep_status": keepStatus,
54 | "debug_enabled": debug,
55 | }).Infoln("Configuration loaded from arguments")
56 |
57 | for {
58 | log.Traceln("Cycle begin.")
59 | cycle()
60 | log.Traceln("Cycle complete.")
61 | time.Sleep(time.Duration(refreshInterval) * time.Second)
62 | }
63 |
64 | }
65 |
66 | func main() {
67 |
68 | log.SetFormatter(&log.TextFormatter{ForceColors: true})
69 | log.SetOutput(colorable.NewColorableStdout())
70 |
71 | app := &cli.App{
72 | Name: name,
73 | Description: "Show your Last.FM scrobbles on Discord!",
74 | Version: version,
75 | Compiled: time.Now(),
76 | Authors: []*cli.Author{
77 | {
78 | Name: "James Ding",
79 | Email: "jamesding365@gmail.com",
80 | },
81 | },
82 | Copyright: "(c) 2022 James Ding",
83 | Flags: []cli.Flag{
84 | &cli.StringFlag{
85 | Name: "user",
86 | Aliases: []string{"u"},
87 | Usage: "Display Last.FM scrobbles from `USERNAME`",
88 | Required: true,
89 | Destination: &username,
90 | },
91 | &cli.IntFlag{
92 | Name: "refresh",
93 | Aliases: []string{"r"},
94 | Usage: "Checks Last.FM every `X` seconds for new scrobbles",
95 | Value: 10,
96 | Destination: &refreshInterval,
97 | },
98 | &cli.BoolFlag{
99 | Name: "hide-profile",
100 | Usage: "Removes buttons to the specified Last.FM profile",
101 | },
102 | &cli.BoolFlag{
103 | Name: "show-loved",
104 | Aliases: []string{"l"},
105 | Usage: "Replaces the default smallImage key with a heart for loved songs.",
106 | },
107 | &cli.BoolFlag{
108 | Name: "rm-covers",
109 | Usage: "Does not show album cover images.",
110 | },
111 | &cli.BoolFlag{
112 | Name: "rm-time",
113 | Usage: "Does not show time elapsed for the scrobble.",
114 | },
115 | &cli.BoolFlag{
116 | Name: "keep-status",
117 | Usage: "Shows status even when there is no active scrobble.",
118 | },
119 | &cli.BoolFlag{
120 | Name: "debug",
121 | Aliases: []string{"d"},
122 | Usage: "Enable verbose and debug logging",
123 | },
124 | },
125 | Action: func(context *cli.Context) error {
126 | return exec(context)
127 | },
128 | }
129 |
130 | err := app.Run(os.Args)
131 | if err != nil {
132 | log.Fatal(err)
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/rpc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/hugolgst/rich-go/client"
5 | "github.com/hugolgst/rich-go/ipc"
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | var loggedIn = false
10 |
11 | func getRPCLogCtx() *log.Entry {
12 | return log.WithFields(log.Fields{
13 | "loggedIn": loggedIn,
14 | })
15 | }
16 |
17 | func login() {
18 | getRPCLogCtx().Traceln("Attempting to close IPC Socket")
19 | err := ipc.CloseSocket()
20 | if err != nil {
21 | getRPCLogCtx().Debugln("IPC Socket Unable to close")
22 | }
23 | err = client.Login(discordAppId)
24 | if err != nil {
25 | getRPCLogCtx().Warnln("Could not login to Discord.")
26 | logout()
27 | } else {
28 | loggedIn = true
29 | getRPCLogCtx().Debugln("Successfully logged into Discord's RPC Server.")
30 | }
31 | }
32 |
33 | func logout() {
34 | client.Logout()
35 | loggedIn = false
36 | getRPCLogCtx().Debugln("Successfully logged out of Discord's RPC Server.")
37 | }
38 |
--------------------------------------------------------------------------------