feat: add all-images and all-videos flags #38
5 changed files with 200 additions and 68 deletions
|
@ -716,16 +716,23 @@ video player in your configuration file respectively.
|
||||||
See the [configuration reference page](configuration.md#integration) on how to set up integration with
|
See the [configuration reference page](configuration.md#integration) on how to set up integration with
|
||||||
your media players.
|
your media players.
|
||||||
|
|
||||||
|
- View a specific media attachment from a specific status
|
||||||
```
|
```
|
||||||
enbas show --type media --from status --status-id 01J0N11V4V7PWH0DDRAVT7TCFK --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT
|
enbas show --type media --from status --status-id 01J0N11V4V7PWH0DDRAVT7TCFK --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT
|
||||||
```
|
```
|
||||||
|
- View all image attachments from a specific status
|
||||||
|
```
|
||||||
|
enbas show --type media --from status --status-id 01J0N11V4V7PWH0DDRAVT7TCFK --all-images
|
||||||
|
```
|
||||||
|
|
||||||
| flag | type | required | description | default |
|
| flag | type | required | description | default |
|
||||||
|------|------|----------|-------------|---------|
|
|------|------|----------|-------------|---------|
|
||||||
| `type` | string | true | The resource you want to view.<br>Here this should be `media`. | |
|
| `type` | string | true | The resource you want to view.<br>Here this should be `media`. | |
|
||||||
| `from` | string | true | The resource you want to view the media from.<br>Here this should be `status`. | |
|
| `from` | string | true | The resource you want to view the media from.<br>Here this should be `status`. | |
|
||||||
| `status-id` | string | true | The ID of the status that you want to view the media from. | |
|
| `status-id` | string | true | The ID of the status that you want to view the media from. | |
|
||||||
| `attachment-id` | string | true | The ID of the media attachment to download and view.<br>Use this flag multiple times to specify multiple media attachments. | |
|
| `attachment-id` | string | false | The ID of the media attachment to download and view.<br>Use this flag multiple times to specify multiple media attachments. | |
|
||||||
|
| `all-images` | boolean | false | Set to `true` to show all images from the status. | false |
|
||||||
|
| `all-videos` | boolean | false | Set to `true` to show all videos from the status. | false |
|
||||||
|
|
||||||
## Bookmarks
|
## Bookmarks
|
||||||
|
|
||||||
|
|
|
@ -111,11 +111,3 @@ type NotFollowingError struct {
|
||||||
func (e NotFollowingError) Error() string {
|
func (e NotFollowingError) Error() string {
|
||||||
return "you are not following " + e.Account
|
return "you are not following " + e.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnknownMediaAttachmentError struct {
|
|
||||||
AttachmentID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e UnknownMediaAttachmentError) Error() string {
|
|
||||||
return "unknown media attachment '" + e.AttachmentID + "'"
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
const (
|
const (
|
||||||
flagAddPoll = "add-poll"
|
flagAddPoll = "add-poll"
|
||||||
flagAccountName = "account-name"
|
flagAccountName = "account-name"
|
||||||
|
flagAllImages = "all-images"
|
||||||
|
flagAllVideos = "all-videos"
|
||||||
flagAttachmentID = "attachment-id"
|
flagAttachmentID = "attachment-id"
|
||||||
flagBrowser = "browser"
|
flagBrowser = "browser"
|
||||||
flagContentType = "content-type"
|
flagContentType = "content-type"
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"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/config"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/media"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/printer"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/printer"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
@ -28,6 +28,8 @@ type ShowExecutor struct {
|
||||||
showUserPreferences bool
|
showUserPreferences bool
|
||||||
showStatuses bool
|
showStatuses bool
|
||||||
skipAccountRelationship bool
|
skipAccountRelationship bool
|
||||||
|
getAllImages bool
|
||||||
|
getAllVideos bool
|
||||||
resourceType string
|
resourceType string
|
||||||
accountName string
|
accountName string
|
||||||
statusID string
|
statusID string
|
||||||
|
@ -58,6 +60,8 @@ func NewShowExecutor(printer *printer.Printer, config *config.Config, name, summ
|
||||||
showExe.BoolVar(&showExe.onlyPinned, flagOnlyPinned, false, "Set to true to show only the account's pinned statuses")
|
showExe.BoolVar(&showExe.onlyPinned, flagOnlyPinned, false, "Set to true to show only the account's pinned statuses")
|
||||||
showExe.BoolVar(&showExe.onlyMedia, flagOnlyMedia, false, "Set to true to show only the statuses with media attachments")
|
showExe.BoolVar(&showExe.onlyMedia, flagOnlyMedia, false, "Set to true to show only the statuses with media attachments")
|
||||||
showExe.BoolVar(&showExe.onlyPublic, flagOnlyPublic, false, "Set to true to show only the account's public posts")
|
showExe.BoolVar(&showExe.onlyPublic, flagOnlyPublic, false, "Set to true to show only the account's public posts")
|
||||||
|
showExe.BoolVar(&showExe.getAllImages, flagAllImages, false, "Set to true to show all images from a status")
|
||||||
|
showExe.BoolVar(&showExe.getAllVideos, flagAllVideos, false, "Set to true to show all videos from a status")
|
||||||
showExe.StringVar(&showExe.resourceType, flagType, "", "Specify the type of resource to display")
|
showExe.StringVar(&showExe.resourceType, flagType, "", "Specify the type of resource to display")
|
||||||
showExe.StringVar(&showExe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)")
|
showExe.StringVar(&showExe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)")
|
||||||
showExe.StringVar(&showExe.statusID, flagStatusID, "", "Specify the ID of the status to display")
|
showExe.StringVar(&showExe.statusID, flagStatusID, "", "Specify the ID of the status to display")
|
||||||
|
@ -525,10 +529,6 @@ func (s *ShowExecutor) showMedia(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error {
|
||||||
if len(s.attachmentIDs) == 0 {
|
|
||||||
return FlagNotSetError{flagText: flagAttachmentID}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.statusID == "" {
|
if s.statusID == "" {
|
||||||
return FlagNotSetError{flagText: flagStatusID}
|
return FlagNotSetError{flagText: flagStatusID}
|
||||||
}
|
}
|
||||||
|
@ -547,65 +547,26 @@ func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err)
|
return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type media struct {
|
mediaBundle := media.NewBundle(
|
||||||
url string
|
cacheDir,
|
||||||
mediaType string
|
status.MediaAttachments,
|
||||||
}
|
s.getAllImages,
|
||||||
|
s.getAllVideos,
|
||||||
attachmentsHashMap := make(map[string]media)
|
s.attachmentIDs,
|
||||||
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 UnknownMediaAttachmentError{AttachmentID: 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 err := mediaBundle.Download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("unable to download the media bundle: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileExists {
|
imageFiles := mediaBundle.ImageFiles()
|
||||||
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 len(imageFiles) > 0 {
|
||||||
if err := utilities.OpenMedia(s.config.Integrations.ImageViewer, imageFiles); err != nil {
|
if err := utilities.OpenMedia(s.config.Integrations.ImageViewer, imageFiles); err != nil {
|
||||||
return fmt.Errorf("unable to open the image viewer: %w", err)
|
return fmt.Errorf("unable to open the image viewer: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
videoFiles := mediaBundle.VideoFiles()
|
||||||
if len(videoFiles) > 0 {
|
if len(videoFiles) > 0 {
|
||||||
if err := utilities.OpenMedia(s.config.Integrations.VideoPlayer, videoFiles); err != nil {
|
if err := utilities.OpenMedia(s.config.Integrations.VideoPlayer, videoFiles); err != nil {
|
||||||
return fmt.Errorf("unable to open the video player: %w", err)
|
return fmt.Errorf("unable to open the video player: %w", err)
|
||||||
|
|
170
internal/media/media.go
Normal file
170
internal/media/media.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mediaTypeImage string = "image"
|
||||||
|
mediaTypeVideo string = "video"
|
||||||
|
)
|
||||||
|
|
||||||
|
type media struct {
|
||||||
|
source string
|
||||||
|
destination string
|
||||||
|
mediaType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *media) download(gtsClient *client.Client) error {
|
||||||
|
fileExists, err := utilities.FileExists(m.destination)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to determine if %s exists: %w",
|
||||||
|
m.destination,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.DownloadMedia(m.source, m.destination); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"downloading %s -> %s failed: %w",
|
||||||
|
m.source,
|
||||||
|
m.destination,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMediaHashmap(cacheDir string, attachments []model.Attachment) map[string]media {
|
||||||
|
hashmap := make(map[string]media)
|
||||||
|
|
||||||
|
for ind := range attachments {
|
||||||
|
hashmap[attachments[ind].ID] = media{
|
||||||
|
source: attachments[ind].URL,
|
||||||
|
destination: mediaFilepath(cacheDir, attachments[ind].URL),
|
||||||
|
mediaType: attachments[ind].Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashmap
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bundle struct {
|
||||||
|
images []media
|
||||||
|
videos []media
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBundle(
|
||||||
|
cacheDir string,
|
||||||
|
attachments []model.Attachment,
|
||||||
|
getAllImages bool,
|
||||||
|
getAllVideos bool,
|
||||||
|
attachmentIDs []string,
|
||||||
|
) Bundle {
|
||||||
|
mediaHashmap := newMediaHashmap(cacheDir, attachments)
|
||||||
|
images := make([]media, 0)
|
||||||
|
videos := make([]media, 0)
|
||||||
|
|
||||||
|
if !getAllImages && !getAllVideos && len(attachmentIDs) == 0 {
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if getAllImages || getAllVideos {
|
||||||
|
if getAllImages {
|
||||||
|
for _, m := range mediaHashmap {
|
||||||
|
if m.mediaType == mediaTypeImage {
|
||||||
|
images = append(images, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if getAllVideos {
|
||||||
|
for _, m := range mediaHashmap {
|
||||||
|
if m.mediaType == mediaTypeVideo {
|
||||||
|
videos = append(videos, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attachmentID := range attachmentIDs {
|
||||||
|
obj, ok := mediaHashmap[attachmentID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch obj.mediaType {
|
||||||
|
case mediaTypeImage:
|
||||||
|
images = append(images, obj)
|
||||||
|
case mediaTypeVideo:
|
||||||
|
videos = append(videos, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) Download(gtsClient *client.Client) error {
|
||||||
|
for ind := range m.images {
|
||||||
|
if err := m.images[ind].download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("received an error trying to download the image files: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ind := range m.videos {
|
||||||
|
if err := m.videos[ind].download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("received an error trying to download the video files: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) ImageFiles() []string {
|
||||||
|
filepaths := make([]string, len(m.images))
|
||||||
|
|
||||||
|
for ind := range m.images {
|
||||||
|
filepaths[ind] = m.images[ind].destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepaths
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) VideoFiles() []string {
|
||||||
|
filepaths := make([]string, len(m.videos))
|
||||||
|
|
||||||
|
for ind := range m.videos {
|
||||||
|
filepaths[ind] = m.videos[ind].destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepaths
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaFilepath(cacheDir, mediaURL string) string {
|
||||||
|
split := strings.Split(mediaURL, "/")
|
||||||
|
|
||||||
|
return filepath.Join(cacheDir, split[len(split)-1])
|
||||||
|
}
|
Loading…
Reference in a new issue