Skip to content

Commit

Permalink
Merge pull request #1 from rekram1-node/working-lib
Browse files Browse the repository at this point in the history
Finalizing 1st Complete version of the library
  • Loading branch information
rekram1-node authored Jan 4, 2023
2 parents f4c7595 + 0a0b772 commit e17b521
Show file tree
Hide file tree
Showing 13 changed files with 502 additions and 72 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/pullRequest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Lint and Test before merging to main

on:
pull_request:
branches:
- main

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19

- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.1
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19

- name: Test
run: |
go test -v ./...
6 changes: 5 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ before:
- go generate ./...

builds:
- skip: true
-
# If true, skip the build.
# Useful for library projects.
# Default is false
skip: true
80 changes: 67 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,87 @@
# blinkgo

![blinkgo](docs/assets/blinkgo-logo.png)
[![Go Report](https://goreportcard.com/badge/github.com/rekram1-node/blinkgo)](https://goreportcard.com/report/github.com/rekram1-node/blinkgo) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rekram1-node/blinkgo/main/LICENSE) ![Build Status](https://github.com/rekram1-node/blinkgo/actions/workflows/main.yml/badge.svg)

CURRENTLY INCOMPLETE, WILL BE READY FOR USAGE IN A COUPLE DAYS
![blinkgo](docs/assets/blinkgo-logo.png)

Simple library for interacting with blink cameras, mainly: authentication, listing devices/networks/clips, and downloading clips from local storage

This library was made for my purposes but if you would like to see more features open an issue and I will get to it

## Installation
Credit to MattTW, who's findings: [BlinkMonitorProtocol](https://github.com/MattTW/BlinkMonitorProtocol) I used to create this implementation

## Features

* authentication
* read networks, cameras, sync modules
* list videos
* download videos

## Getting Started

### Prerequisites
- [Go](https://go.dev/)

### Getting blinkgo

With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import

```go
import "github.com/rekram1-node/blinkgo/blink"
```

to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies.

Otherwise, run the following to install the `blinkgo` library

```shell
go get -u github.com/rekram1-node/blinkgo/blink
$ go get -u github.com/rekram1-node/blinkgo/blink
```

## Table of contents
## Documentation

* [authentication](#auth)
* [fetch user info](#user)
* [list cameras](#camera)
* [download videos](#videos)
Read the [documentation](https:/github.com/rekram1-node/blinkgo/docs/docs.md) for usage instructions

## Usage

```shell
example
## Authentication

#### Simple login and 2FA verify pin
```go
package main

import (
"fmt"
"log"

"github.com/rekram1-node/blinkgo/blink"
)

func main() {
email := "[email protected]"
password := "PLEASE_DON'T_PLAINTEXT_REAL_PASSWORDS"

// returns account object with: email, password, uuid
account := blink.NewAccount(email, password)

// this returns a login response that you can use
// however, it is unneccessary for this example
if _, err := account.Login(); err != nil {
log.Fatal(err)
}

fmt.Print("Enter Pin: ")
var pin string
fmt.Scanln(&pin)

// this returns a verify pin response that you can use
// however, it is unneccessary for this example
if _, err = account.VerifyPin(pin); err != nil {
log.Fatal(err)
}
}
```

## Local Storage
## Local-Storage

I did not discover this myself, this is from [blinkpy](https://github.com/fronzbot/blinkpy)

Expand Down
4 changes: 1 addition & 3 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ tasks:
test:
desc: "runs unit tests"
cmds:
- go test -v
sources:
- ./**/*.go
- go test -v ./...

build:
cmds:
Expand Down
35 changes: 35 additions & 0 deletions blink/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package blink_test

import (
"testing"

"github.com/rekram1-node/blinkgo/blink"
"github.com/stretchr/testify/assert"
)

func TestNewAccount(t *testing.T) {
tests := []struct {
name string
fakeEmail string
fakePass string
expectedAccount *blink.Account
}{
{
name: "NewAccount: Good Call",
fakeEmail: "[email protected]",
fakePass: "fakepassword",
expectedAccount: &blink.Account{
Email: "[email protected]",
Password: "fakepassword",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
account := blink.NewAccount(test.fakeEmail, test.fakePass)
assert.Equal(t, test.expectedAccount.Email, account.Email)
assert.Equal(t, test.expectedAccount.Password, account.Password)
assert.NotEqual(t, "", account.UniqueID)
})
}
}
1 change: 0 additions & 1 deletion blink/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ func (account *Account) updateAccountInfo(resp *LoginResponse) {
}

func (account *Account) RefreshToken() (*LoginResponse, error) {
// this method is incomplete
// this just for readability of code I created RefreshToken
return account.Login()
}
Expand Down
15 changes: 0 additions & 15 deletions blink/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,6 @@ func (account *Account) GetManifest() (*DeviceManifest, error) {
return manifest, nil
}

func (account *Account) GetSyncModules() error {
if account.SyncModules != nil {
return nil
}

manifest, err := account.GetManifest()

if err != nil {
return err
}

account.SyncModules = &manifest.SyncModules
return nil
}

type localStorageManifestIDResponse struct {
ID int `json:"id"`
NetworkID int `json:"network_id"`
Expand Down
12 changes: 12 additions & 0 deletions blink/syncmodule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package blink

func (account *Account) GetSyncModules() error {
manifest, err := account.GetManifest()

if err != nil {
return err
}

account.SyncModules = &manifest.SyncModules
return nil
}
97 changes: 60 additions & 37 deletions blink/video.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"fmt"
"io"
"os"
"strings"
"time"

"github.com/go-resty/resty/v2"
"github.com/rekram1-node/blinkgo/internal/client"
)

Expand Down Expand Up @@ -41,45 +43,22 @@ func (account *Account) GetVideoEvents(pages int) (*VideoEvents, error) {
return videoEvents, nil
}

func (account *Account) GetClipIDs(networkID, syncModuleID, requestID int) (*[]Clip, error) {
func (account *Account) GetClipIDs(networkID, syncModuleID, requestID int) (*[]Clip, string, error) {
manifest, err := account.GetLocalStorageManifest(networkID, syncModuleID, requestID)

if err != nil {
return nil, err
return nil, "", err
}

return &manifest.Clips, nil
return &manifest.Clips, manifest.ManifestID, nil
}

func (account *Account) DownloadVideoByClipID(networkID, syncModuleID int, manifestID, clipID, fileName string) error {
// this filename should have the ".mp4" in it
out, err := os.Create(fileName)

if err != nil {
return err
}

defer out.Close()

// filename should have the .mp4 included in it
c := client.New(account.AuthToken)
url := fmt.Sprintf("https://rest-%s.immedia-semi.com/api/v1/accounts/%d/networks/%d/sync_modules/%d/local_storage/manifest/%s/clip/request/%s", account.Tier, account.ID, networkID, syncModuleID, manifestID, clipID)

resp, err := c.R().
SetDoNotParseResponse(true). // necessary to read file
Get(url)

if err != nil {
return err
} else if !resp.IsSuccess() {
return fmt.Errorf("failed to get request download clip by ID: %s, status code: %d, response: %s", clipID, resp.StatusCode(), string(resp.Body()))
}

// copy mp4 video into file
if _, err = io.Copy(out, resp.RawBody()); err != nil {
return err
}

return resp.RawBody().Close()
return downloadVideo(url, fileName, c)
}

type UploadResponse struct {
Expand All @@ -106,11 +85,11 @@ func (account *Account) RequestUploadByClipID(networkID, syncModuleID int, manif
return uploadRes, nil
}

type Media struct {
type AllMedia struct {
Limit int64 `json:"limit"`
PurgeID int64 `json:"purge_id"`
RefreshCount int64 `json:"refresh_count"`
Media []struct {
MediaList []struct {
AdditionalDevices []interface{} `json:"additional_devices"`
CreatedAt string `json:"created_at"`
Deleted bool `json:"deleted"`
Expand Down Expand Up @@ -159,11 +138,10 @@ type Video struct {
Viewed string `json:"viewed"`
}

func (account *Account) GetMedia(daysBack, pageNum int) (*Media, error) {
currTimeStamp := time.Now().UTC().Add(-time.Hour * 24 * time.Duration(daysBack)).Format("2006-01-02T15:04:05-0700")
media := &Media{}
func (account *Account) GetMedia(sinceTimestamp string, pageNum int) (*AllMedia, error) {
media := &AllMedia{}
c := client.New(account.AuthToken)
url := fmt.Sprintf("/api/v1/accounts/%d/media/changed?since=%s&page=%d", account.ID, currTimeStamp, pageNum)
url := fmt.Sprintf("/api/v1/accounts/%d/media/changed?since=%s&page=%d", account.ID, sinceTimestamp, pageNum)

resp, err := c.R().
SetResult(media).
Expand All @@ -178,8 +156,8 @@ func (account *Account) GetMedia(daysBack, pageNum int) (*Media, error) {
return media, nil
}

func (account *Account) GetVideos(daysBack, pageNum int) (*[]Video, error) {
media, err := account.GetMedia(daysBack, pageNum)
func (account *Account) GetVideos(sinceTimestamp string, pageNum int) (*[]Video, error) {
media, err := account.GetMedia(sinceTimestamp, pageNum)

if err != nil {
return nil, err
Expand All @@ -188,6 +166,51 @@ func (account *Account) GetVideos(daysBack, pageNum int) (*[]Video, error) {
return &media.Videos, nil
}

func (account *Account) DownloadVideosByPage() {
func (account *Account) DownloadVideosByPages(pages int, downloadDir string) error {
timeStamp := "1970-01-01T00:00Z"
allMedia, err := account.GetMedia(timeStamp, pages)

if err != nil {
return err
}

for _, media := range allMedia.MediaList {
baseURL := strings.ReplaceAll(client.BaseURL, "prod", account.Tier)
mp4URL := baseURL + media.Media
fileName := downloadDir + "/" + media.DeviceName + "-" + media.NetworkName + "-" + media.CreatedAt + ".mp4"
c := client.New(account.AuthToken)

if err = downloadVideo(mp4URL, fileName, c); err != nil {
return err
}
}

return nil
}

func downloadVideo(url, file string, c *resty.Client) error {
out, err := os.Create(file)

if err != nil {
return err
}

defer out.Close()

resp, err := c.R().
SetDoNotParseResponse(true). // necessary to read file
Get(url)

if err != nil {
return err
} else if !resp.IsSuccess() {
return fmt.Errorf("failed to download clip from: %s, status code: %d, response: %s", url, resp.StatusCode(), string(resp.Body()))
}

// copy mp4 video into file
if _, err = io.Copy(out, resp.RawBody()); err != nil {
return err
}

return resp.RawBody().Close()
}
Loading

0 comments on commit e17b521

Please sign in to comment.