Skip to content

Commit

Permalink
added multimedia support
Browse files Browse the repository at this point in the history
  • Loading branch information
beebeeoii committed Jan 22, 2022
1 parent abada5d commit a184f13
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 2 deletions.
2 changes: 1 addition & 1 deletion FyneApp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Website = "https://github.com/beebeeoii/lominus"
Name = "Lominus"
ID = "com.beebeeoii.lominus"
Version = "1.2.0"
Build = 121
Build = 153
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
fyne.io/fyne/v2 v2.1.1
github.com/go-co-op/gocron v1.10.0
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
github.com/sirupsen/logrus v1.8.1
github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91
)

Expand All @@ -19,7 +20,6 @@ require (
github.com/go-stack/stack v1.8.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
Expand Down
53 changes: 53 additions & 0 deletions pkg/api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,28 @@ type ModuleRequest struct {
Request Request
}

// MultimediaChannelRequest struct is the datapack for containing details about a specific HTTP request used for multimedia channels (Luminus Multimedia).
type MultimediaChannelRequest struct {
Module Module
Request Request
}

// MultimediaVideoRequest struct is the datapack for containing details about a specific HTTP request used for multimedia video (Luminus Multimedia).
type MultimediaVideoRequest struct {
MultimediaChannel MultimediaChannel
Request Request
}

const (
GET_ALL_FOLDERS = 0
GET_ALL_FILES = 1
DOWNLOAD_FILE = 2
)

const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0"
const POST = "POST"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_JSON = "application/json; charset=UTF-8"

// BuildModuleRequest builds and returns a ModuleRequest that can be used for Module related operations
// such as retrieving all modules.
Expand Down Expand Up @@ -80,6 +95,44 @@ func BuildGradeRequest(module Module) (GradeRequest, error) {
}, nil
}

// BuildMultimediaChannelRequest builds and returns a MultimediaChannelRequuest that can be used for Multimedia
// channel related operations such as retrieving all channels of a module.
// A Module is required to build a BuildMultimediaChannelRequest as it is module specific.
func BuildMultimediaChannelRequest(module Module) (MultimediaChannelRequest, error) {
jwtToken, jwtTokenErr := retrieveJwtToken()
if jwtTokenErr != nil {
return MultimediaChannelRequest{}, jwtTokenErr
}

return MultimediaChannelRequest{
Module: module,
Request: Request{
Url: fmt.Sprintf(MULTIMEMDIA_CHANNEL_URL_ENDPOINT, module.Id),
JwtToken: jwtToken,
UserAgent: USER_AGENT,
},
}, nil
}

// BuildMultimediaChannelRequest builds and returns a MultimediaChannelRequuest that can be used for Multimedia
// channel related operations such as retrieving all channels of a module.
// A Module is required to build a BuildMultimediaChannelRequest as it is module specific.
func BuildMultimediaVideoRequest(multimediaChannel MultimediaChannel) (MultimediaVideoRequest, error) {
jwtToken, jwtTokenErr := retrieveJwtToken()
if jwtTokenErr != nil {
return MultimediaVideoRequest{}, jwtTokenErr
}

return MultimediaVideoRequest{
MultimediaChannel: multimediaChannel,
Request: Request{
Url: fmt.Sprintf(LTI_DATA_URL_ENDPOINT, multimediaChannel.Id),
JwtToken: jwtToken,
UserAgent: USER_AGENT,
},
}, nil
}

// BuildDocumentRequest builds and returns a DocumentRequest that can be used for File/Folder related operations
// such as retrieving files/folders of a module.
// DocumentRequests must be built using Module/Folder/File only.
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ type DownloadResponse struct {
DownloadUrl string `json:"data"`
}

// LTIDataResponse struct is the datapack for containing API response Panapto LTI data.
type LTIDataResponse struct {
DataItems []map[string]interface{} `json:"dataItems"`
Html string `json:"html"`
LaunchURL string `json:"launchURL"`
}

// GetRawResponse sends the HTTP request and marshals it into the pointer provided.
// Argument provided must be a pointer.
func (req Request) GetRawResponse(res interface{}) error {
Expand Down
223 changes: 223 additions & 0 deletions pkg/api/videos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Package api provides functions that link up and communicate with Luminus servers.
package api

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)

// MultimediaChannel struct is the datapack for containing details about every multimedia channel in a module.
type MultimediaChannel struct {
Id string
Name string
MediaCount int
LastUpdated int64
}

// MultimediaVideo struct is the datapack for containing details about every multimedia video in a module.
type MultimediaVideo struct {
Id string
Name string
FolderId string // Also known as Channel Id
FolderName string // Also known as Channel Name
M3u8Url string
}

type panaptoCredentials struct {
aspAuth string
csrfToken string
folderId string
}

type PanaptoVideoRawResponse struct {
D PanaptoVideoResponse `json:"d"`
}

type PanaptoVideoResponse struct {
Results []map[string]interface{} `json:"Results"`
Total int `json:"TotalNumber"`
}

const MULTIMEMDIA_CHANNEL_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/multimedia/?populate=contentSummary&ParentID=%s"
const LTI_DATA_URL_ENDPOINT = "https://luminus.nus.edu.sg/v2/api/lti/Launch/mediaweb?context_id=%s&returnURL=https://luminus.nus.edu.sg/iframe/lti-return/mediaweb"

const PANAPTO_AUTH_URL_ENDPOINT = "https://mediaweb.ap.panopto.com/Panopto/LTI/LTI.aspx"
const PANAPTO_VIDEOS_URL_ENDPOINT = "https://mediaweb.ap.panopto.com/Panopto/Services/Data.svc/GetSessions"
const PANAPTO_VIDEO_DELIVERY_URL_ENDPOINT = "https://mediaweb.ap.panopto.com/Panopto/Pages/Viewer/DeliveryInfo.aspx"

const PANAPTO_ASPAUTH_KEY = ".ASPXAUTH"
const PANAPTO_CSRF_KEY = "csrfToken"

// getMultimediaChannelFieldsRequired is a helper function that returns a constant array with fields that a multimedia channel element
// returned by Luminus needs.
func getMultimediaChannelFieldsRequired() []string {
return []string{"access", "id", "name", "mediaCount", "lastUpdatedDate"}
}

func (req MultimediaChannelRequest) GetMultimediaChannels() ([]MultimediaChannel, error) {
var multimediaChannels []MultimediaChannel

rawResponse := RawResponse{}
err := req.Request.GetRawResponse(&rawResponse)
if err != nil {
return multimediaChannels, err
}

for _, content := range rawResponse.Data {
if !IsResponseValid(getMultimediaChannelFieldsRequired(), content) {
continue
}

if _, exists := content["access"]; exists {
lastUpdated := int64(-1)
lastUpdatedTime, err := time.Parse(time.RFC3339, content["lastUpdatedDate"].(string))
if err != nil {
return multimediaChannels, err
}
lastUpdated = lastUpdatedTime.Unix()

multimediaChannel := MultimediaChannel{
Id: content["id"].(string),
Name: content["name"].(string),
MediaCount: int(content["mediaCount"].(float64)),
LastUpdated: lastUpdated,
}

multimediaChannels = append(multimediaChannels, multimediaChannel)
}
}

return multimediaChannels, nil
}

func (req MultimediaVideoRequest) GetMultimediaVideos() ([]MultimediaVideo, error) {
panaptoAuthReqBody := url.Values{}

var multimediaVideos []MultimediaVideo

ltiDataResponse := LTIDataResponse{}
err := req.Request.GetRawResponse(&ltiDataResponse)
if err != nil {
return multimediaVideos, err
}

for _, content := range ltiDataResponse.DataItems {
panaptoAuthReqBody.Set(content["key"].(string), content["value"].(string))
}

panaptoCredentials, getPanaptoCredentialsErr := getPanaptoCredentials(panaptoAuthReqBody)
if getPanaptoCredentialsErr != nil {
return multimediaVideos, err
}

return getPanaptoVideos(panaptoCredentials)
}

func getPanaptoCredentials(data url.Values) (panaptoCredentials, error) {
var panaptoCredentials panaptoCredentials
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}

panaptoAuthReq, panaptoAuthReqErr := http.NewRequest(POST, PANAPTO_AUTH_URL_ENDPOINT, strings.NewReader(data.Encode()))
if panaptoAuthReqErr != nil {
return panaptoCredentials, panaptoAuthReqErr
}

panaptoAuthReq.Header.Add("Content-Type", CONTENT_TYPE_FORM)
panaptoAuthReq.Header.Add("User-Agent", USER_AGENT)

authRes, authResErr := client.Do(panaptoAuthReq)
if authResErr != nil {
return panaptoCredentials, authResErr
}

if len(authRes.Cookies()) < 2 {
return panaptoCredentials, fmt.Errorf("unable to get Panapto credentials - invalid auth res cookies received")
}

for _, cookie := range authRes.Cookies() {
if cookie.Name == PANAPTO_ASPAUTH_KEY {
panaptoCredentials.aspAuth = cookie.Value
}

if cookie.Name == PANAPTO_CSRF_KEY {
panaptoCredentials.csrfToken = cookie.Value
}
}

location := authRes.Header.Get("Location")

indexStart := strings.Index(location, "folderID%3D") + 11
indexEnd := strings.Index(location, "%26isLTIEmbed")
panaptoCredentials.folderId = location[indexStart:indexEnd]

return panaptoCredentials, nil
}

func getPanaptoVideos(credentials panaptoCredentials) ([]MultimediaVideo, error) {
var multimediaVideos []MultimediaVideo

jsonBody := map[string]map[string]string{
"queryParameters": {
"folderID": credentials.folderId,
},
}
jsonValue, _ := json.Marshal(jsonBody)
client := &http.Client{}

panaptoVideoReq, panaptoVideoReqErr := http.NewRequest(POST, PANAPTO_VIDEOS_URL_ENDPOINT, bytes.NewBuffer(jsonValue))
if panaptoVideoReqErr != nil {
return multimediaVideos, panaptoVideoReqErr
}

panaptoVideoReq.Header.Add("Content-Type", CONTENT_TYPE_JSON)
panaptoVideoReq.Header.Add("User-Agent", USER_AGENT)
panaptoVideoReq.AddCookie(&http.Cookie{
Name: ".ASPXAUTH",
Value: credentials.aspAuth,
})
panaptoVideoReq.AddCookie(&http.Cookie{
Name: "csrfToken",
Value: credentials.csrfToken,
})

videoRes, videoResErr := client.Do(panaptoVideoReq)
if videoResErr != nil {
return multimediaVideos, videoResErr
}

body, bodyErr := ioutil.ReadAll(videoRes.Body)
if bodyErr != nil {
return multimediaVideos, bodyErr
}

var rawResponse PanaptoVideoRawResponse
json.Unmarshal(body, &rawResponse)

for _, video := range rawResponse.D.Results {
multimediaVideo := MultimediaVideo{
Id: video["SessionID"].(string),
Name: video["SessionName"].(string),
FolderId: video["FolderID"].(string),
FolderName: video["FolderName"].(string),
M3u8Url: video["IosVideoUrl"].(string),
}

multimediaVideos = append(multimediaVideos, multimediaVideo)
}

return multimediaVideos, nil
}

// func (video MultimediaVideo) Download(dir string) error {
// m3u8.
// }

0 comments on commit a184f13

Please sign in to comment.