feat: view media with external applications #31
10 changed files with 297 additions and 52 deletions
|
@ -30,13 +30,19 @@ func main() {
|
||||||
func run() error {
|
func run() error {
|
||||||
var (
|
var (
|
||||||
configDir string
|
configDir string
|
||||||
|
cacheDir string
|
||||||
pager string
|
pager string
|
||||||
|
imageViewer string
|
||||||
|
videoPlayer string
|
||||||
maxTerminalWidth int
|
maxTerminalWidth int
|
||||||
noColor *bool
|
noColor *bool
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&configDir, "config-dir", "", "Specify your config directory")
|
flag.StringVar(&configDir, "config-dir", "", "Specify your config directory")
|
||||||
|
flag.StringVar(&cacheDir, "cache-dir", "", "Specify your cache directory")
|
||||||
flag.StringVar(&pager, "pager", "", "Specify your preferred pager to page through long outputs. This is disabled by default.")
|
flag.StringVar(&pager, "pager", "", "Specify your preferred pager to page through long outputs. This is disabled by default.")
|
||||||
|
flag.StringVar(&imageViewer, "image-viewer", "", "Specify your favourite image viewer.")
|
||||||
|
flag.StringVar(&videoPlayer, "video-player", "", "Specify your favourite video player.")
|
||||||
flag.IntVar(&maxTerminalWidth, "max-terminal-width", 80, "Specify the maximum terminal width when displaying resources on screen.")
|
flag.IntVar(&maxTerminalWidth, "max-terminal-width", 80, "Specify the maximum terminal width when displaying resources on screen.")
|
||||||
|
|
||||||
flag.BoolFunc("no-color", "Disable ANSI colour output when displaying text on screen", func(value string) error {
|
flag.BoolFunc("no-color", "Disable ANSI colour output when displaying text on screen", func(value string) error {
|
||||||
|
@ -170,6 +176,9 @@ func run() error {
|
||||||
executor.CommandShow: executor.NewShowExecutor(
|
executor.CommandShow: executor.NewShowExecutor(
|
||||||
printer,
|
printer,
|
||||||
configDir,
|
configDir,
|
||||||
|
cacheDir,
|
||||||
|
imageViewer,
|
||||||
|
videoPlayer,
|
||||||
executor.CommandShow,
|
executor.CommandShow,
|
||||||
executor.CommandSummaryLookup(executor.CommandShow),
|
executor.CommandSummaryLookup(executor.CommandShow),
|
||||||
),
|
),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
@ -60,13 +61,50 @@ func (g *Client) AuthCodeURL() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Client) DownloadMedia(url, path string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("User-Agent", g.UserAgent)
|
||||||
|
|
||||||
|
response, err := g.HTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error after attempting the download: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"did not receive an OK response from the GoToSocial server: got %d",
|
||||||
|
response.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(file, response.Body); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the download to %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Client) sendRequest(method string, url string, requestBody io.Reader, object any) error {
|
func (g *Client) sendRequest(method string, url string, requestBody io.Reader, object any) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, method, url, requestBody)
|
request, err := http.NewRequestWithContext(ctx, method, url, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the HTTP request, %w", err)
|
return fmt.Errorf("unable to create the HTTP request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -41,7 +43,7 @@ func (e CredentialsNotFoundError) Error() string {
|
||||||
// directory. If the directory is not specified then the default directory is used. If the directory
|
// directory. If the directory is not specified then the default directory is used. If the directory
|
||||||
// is not present, it will be created.
|
// is not present, it will be created.
|
||||||
func SaveCredentials(configDir, username string, credentials Credentials) (string, error) {
|
func SaveCredentials(configDir, username string, credentials Credentials) (string, error) {
|
||||||
if err := ensureConfigDir(calculateConfigDir(configDir)); err != nil {
|
if err := utilities.EnsureDirectory(utilities.CalculateConfigDir(configDir)); err != nil {
|
||||||
return "", fmt.Errorf("unable to ensure the configuration directory: %w", err)
|
return "", fmt.Errorf("unable to ensure the configuration directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,5 +143,5 @@ func saveCredentialsConfigFile(authConfig CredentialsConfig, configDir string) e
|
||||||
}
|
}
|
||||||
|
|
||||||
func credentialsConfigFile(configDir string) string {
|
func credentialsConfigFile(configDir string) string {
|
||||||
return filepath.Join(calculateConfigDir(configDir), credentialsFileName)
|
return filepath.Join(utilities.CalculateConfigDir(configDir), credentialsFileName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func calculateConfigDir(configDir string) string {
|
|
||||||
if configDir != "" {
|
|
||||||
return configDir
|
|
||||||
}
|
|
||||||
|
|
||||||
rootDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
rootDir = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(rootDir, internal.ApplicationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigDir(configDir string) error {
|
|
||||||
if _, err := os.Stat(configDir); err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
|
||||||
return fmt.Errorf("unable to create %s: %w", configDir, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unknown error received after getting the config directory information: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -7,6 +7,8 @@ package executor
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
@ -23,6 +25,7 @@ type ShowExecutor struct {
|
||||||
showUserPreferences bool
|
showUserPreferences bool
|
||||||
showInBrowser bool
|
showInBrowser bool
|
||||||
configDir string
|
configDir string
|
||||||
|
cacheRoot string
|
||||||
resourceType string
|
resourceType string
|
||||||
accountName string
|
accountName string
|
||||||
statusID string
|
statusID string
|
||||||
|
@ -30,16 +33,22 @@ type ShowExecutor struct {
|
||||||
listID string
|
listID string
|
||||||
tag string
|
tag string
|
||||||
pollID string
|
pollID string
|
||||||
attachmentID string
|
fromResourceType string
|
||||||
|
imageViewer string
|
||||||
|
videoPlayer string
|
||||||
limit int
|
limit int
|
||||||
|
attachmentIDs MultiStringFlagValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShowExecutor(printer *printer.Printer, configDir, name, summary string) *ShowExecutor {
|
func NewShowExecutor(printer *printer.Printer, configDir, cacheRoot, imageViewer, videoPlayer, name, summary string) *ShowExecutor {
|
||||||
showExe := ShowExecutor{
|
showExe := ShowExecutor{
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
|
||||||
printer: printer,
|
printer: printer,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
|
cacheRoot: cacheRoot,
|
||||||
|
imageViewer: imageViewer,
|
||||||
|
videoPlayer: videoPlayer,
|
||||||
}
|
}
|
||||||
|
|
||||||
showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "Set to true to lookup your account")
|
showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "Set to true to lookup your account")
|
||||||
|
@ -53,7 +62,8 @@ func NewShowExecutor(printer *printer.Printer, configDir, name, summary string)
|
||||||
showExe.StringVar(&showExe.listID, flagListID, "", "Specify the ID of the list to display")
|
showExe.StringVar(&showExe.listID, flagListID, "", "Specify the ID of the list to display")
|
||||||
showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use")
|
showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use")
|
||||||
showExe.StringVar(&showExe.pollID, flagPollID, "", "Specify the ID of the poll to display")
|
showExe.StringVar(&showExe.pollID, flagPollID, "", "Specify the ID of the poll to display")
|
||||||
showExe.StringVar(&showExe.attachmentID, flagAttachmentID, "", "Specify the ID of the media attachment to display")
|
showExe.Var(&showExe.attachmentIDs, flagAttachmentID, "Specify the ID of the media attachment to display")
|
||||||
|
showExe.StringVar(&showExe.fromResourceType, flagFrom, "", "Specify the resource type to view the target resource from (e.g. status for viewing media from, etc)")
|
||||||
showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display")
|
showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display")
|
||||||
|
|
||||||
showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet)
|
showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet)
|
||||||
|
@ -81,7 +91,7 @@ func (s *ShowExecutor) Execute() error {
|
||||||
resourceFollowRequest: s.showFollowRequests,
|
resourceFollowRequest: s.showFollowRequests,
|
||||||
resourcePoll: s.showPoll,
|
resourcePoll: s.showPoll,
|
||||||
resourceMutedAccounts: s.showMutedAccounts,
|
resourceMutedAccounts: s.showMutedAccounts,
|
||||||
resourceMedia: s.showMediaAttachment,
|
resourceMedia: s.showMedia,
|
||||||
resourceMediaAttachment: s.showMediaAttachment,
|
resourceMediaAttachment: s.showMediaAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,11 +418,18 @@ func (s *ShowExecutor) showMutedAccounts(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showMediaAttachment(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showMediaAttachment(gtsClient *client.Client) error {
|
||||||
if s.attachmentID == "" {
|
if len(s.attachmentIDs) == 0 {
|
||||||
return FlagNotSetError{flagText: flagAttachmentID}
|
return FlagNotSetError{flagText: flagAttachmentID}
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment, err := gtsClient.GetMediaAttachment(s.attachmentID)
|
if len(s.attachmentIDs) != 1 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unexpected number of attachment IDs received: want 1, got %d",
|
||||||
|
len(s.attachmentIDs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := gtsClient.GetMediaAttachment(s.attachmentIDs[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve the media attachment: %w", err)
|
return fmt.Errorf("unable to retrieve the media attachment: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -421,3 +438,111 @@ func (s *ShowExecutor) showMediaAttachment(gtsClient *client.Client) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMedia(gtsClient *client.Client) error {
|
||||||
|
if s.fromResourceType == "" {
|
||||||
|
return FlagNotSetError{flagText: flagFrom}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceStatus: s.showMediaFromStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[s.fromResourceType]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("do not support viewing media from %s", s.fromResourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error {
|
||||||
|
if len(s.attachmentIDs) == 0 {
|
||||||
|
return FlagNotSetError{flagText: flagAttachmentID}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.statusID == "" {
|
||||||
|
return FlagNotSetError{flagText: flagStatusID}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(s.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(
|
||||||
|
utilities.CalculateCacheDir(s.cacheRoot, utilities.GetFQDN(gtsClient.Authentication.Instance)),
|
||||||
|
"media",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := utilities.EnsureDirectory(cacheDir); err != nil {
|
||||||
|
return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type media struct {
|
||||||
|
url string
|
||||||
|
mediaType string
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsHashMap := make(map[string]media)
|
||||||
|
imageFiles := make([]string, 0)
|
||||||
|
videoFiles := make([]string, 0)
|
||||||
|
|
||||||
|
for _, statusAttachment := range status.MediaAttachments {
|
||||||
|
attachmentsHashMap[statusAttachment.ID] = media{
|
||||||
|
url: statusAttachment.URL,
|
||||||
|
mediaType: statusAttachment.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attachmentID := range s.attachmentIDs {
|
||||||
|
mediaObj, ok := attachmentsHashMap[attachmentID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown media attachment: %s", attachmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(mediaObj.url, "/")
|
||||||
|
filename := split[len(split)-1]
|
||||||
|
filePath := filepath.Join(cacheDir, filename)
|
||||||
|
|
||||||
|
fileExists, err := utilities.FileExists(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to check if the media file is already downloaded for %s: %w",
|
||||||
|
attachmentID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileExists {
|
||||||
|
if err := gtsClient.DownloadMedia(mediaObj.url, filePath); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to download the media attachment for %s: %w",
|
||||||
|
attachmentID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mediaObj.mediaType {
|
||||||
|
case "image":
|
||||||
|
imageFiles = append(imageFiles, filePath)
|
||||||
|
case "video":
|
||||||
|
videoFiles = append(videoFiles, filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(imageFiles) > 0 {
|
||||||
|
if err := utilities.OpenMedia(s.imageViewer, imageFiles); err != nil {
|
||||||
|
return fmt.Errorf("unable to open the image viewer: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(videoFiles) > 0 {
|
||||||
|
if err := utilities.OpenMedia(s.videoPlayer, videoFiles); err != nil {
|
||||||
|
return fmt.Errorf("unable to open the video player: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package printer
|
package printer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
54
internal/utilities/directories.go
Normal file
54
internal/utilities/directories.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package utilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CalculateConfigDir(configDir string) string {
|
||||||
|
if configDir != "" {
|
||||||
|
return configDir
|
||||||
|
}
|
||||||
|
|
||||||
|
configRoot, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
configRoot = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(configRoot, internal.ApplicationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateCacheDir(cacheDir, instanceFQDN string) string {
|
||||||
|
if cacheDir != "" {
|
||||||
|
return cacheDir
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheRoot, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
cacheRoot = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(cacheRoot, internal.ApplicationName, instanceFQDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureDirectory(dir string) error {
|
||||||
|
if _, err := os.Stat(dir); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
|
return fmt.Errorf("unable to create %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("received an unknown error after getting the directory information: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
package utilities
|
package utilities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
@ -17,3 +18,15 @@ func ReadFile(path string) (string, error) {
|
||||||
|
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FileExists(path string) (bool, error) {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("unable to check if the file exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
37
internal/utilities/utilities.go
Normal file
37
internal/utilities/utilities.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package utilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFQDN(url string) string {
|
||||||
|
r := regexp.MustCompile(`http(s)?:\/\/`)
|
||||||
|
|
||||||
|
return r.ReplaceAllString(url, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnspecifiedProgramError struct{}
|
||||||
|
|
||||||
|
func (e UnspecifiedProgramError) Error() string {
|
||||||
|
return "the program to view these files is unspecified"
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenMedia(viewer string, paths []string) error {
|
||||||
|
if viewer == "" {
|
||||||
|
return UnspecifiedProgramError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command(viewer, paths...)
|
||||||
|
|
||||||
|
if err := command.Start(); err != nil {
|
||||||
|
return fmt.Errorf("received an error after starting the image viewer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue