├── .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 | go-http-client 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 | GitHub all releases 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 | --------------------------------------------------------------------------------