├── README.md └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # rm-zoterosync 2 | 3 | I made this because I wanted to be able to get my zotero library onto my reMarkable and I did not want to use a cloud server to do so. 4 | This is possible because you can ssh into a reMarkable and run any executable that is compiled for Linux ARMv7. 5 | 6 | ## How to run it on your reMarkable 7 | 8 | Download the executable from [here](https://github.com/Maaarcocr/rm-zoterosync/releases/download/0.1.2/rm-zoterosync). Copy it on your reMarkable using `scp` and run it with `rm-zoterosync &` so that it executes in the backgroud. 9 | 10 | You have to specify 2 environment variables before running it on your reMarkable: `ZOTERO_USERID` (which is not your username and you can find it at [here](https://www.zotero.org/settings/keys)) and `ZOTERO_APIKEY`. 11 | 12 | ## Some weird decisions I took 13 | 14 | - The go code will only sync your zotero collections for which a folder in your remarkable exists with the exact same name. 15 | - All the files that are downloaded from Zotero are saved in `My Files` and not inside any folder, this is due to the fact that I can't specify the folder I want to use when using the upload API provided by the WebUI. 16 | - All the files that you want to sync should have a public URL present in their Zotero metadata. (This could be modified if this utility downloaded the files directly from Zotero, but it was not neccessary for me) 17 | 18 | ## Compile from source 19 | 20 | If you want to compile the source code on your own you have to use the Go compiler and set these environment variables before compiling: `GOOS="linux"`, `GOARCH="arm"` and `GOARM=7` 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/Maaarcocr/rmsync" 11 | "github.com/peterhellberg/link" 12 | ) 13 | 14 | var UserId string = os.Getenv("ZOTERO_USERID") 15 | var ApiKey string = os.Getenv("ZOTERO_APIKEY") 16 | var myClient = &http.Client{Timeout: 10 * time.Second} 17 | 18 | const BaseZoteroURL string = "https://api.zotero.org/users/" 19 | 20 | const BaseDir string = "/home/root/.local/share/remarkable/xochitl/" 21 | 22 | type Metadata struct { 23 | Deleted bool `json:"deleted"` 24 | DastModified string `json:"lastModified"` 25 | Metadatamodified bool `json:"metadatamodified"` 26 | Modified bool `json:"modified"` 27 | Parent string `json:"parent"` 28 | Pinned bool `json:"pinned"` 29 | Synced bool `json:"synced"` 30 | Type string `json:"type"` 31 | Version int `json:"version"` 32 | VisibleName string `json:"visibleName"` 33 | } 34 | 35 | type ZoteroItem struct { 36 | Key string `json:"key"` 37 | Data ZoteroItemData `json:"data"` 38 | } 39 | 40 | type ZoteroItemData struct { 41 | ContentType string `json:"contentType"` 42 | Filename string `json:"filename"` 43 | Url string `json:"url"` 44 | } 45 | 46 | type ZoteroDirectory struct { 47 | Key string `json:"key"` 48 | Data ZoteroDirData `json:"data"` 49 | } 50 | 51 | type ZoteroDirData struct { 52 | Key string `json:"key"` 53 | Version int `json:"version"` 54 | Name string `json:"name"` 55 | ParentCollection bool `json:"parentCollection"` 56 | Relations interface{} `json:"relations"` 57 | } 58 | 59 | func getJson(url string, target interface{}) (*http.Response, error) { 60 | req, _ := http.NewRequest("GET", url, nil) 61 | req.Header.Set("Zotero-API-Key", ApiKey) 62 | res, err := myClient.Do(req) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer res.Body.Close() 67 | 68 | return res, json.NewDecoder(res.Body).Decode(target) 69 | } 70 | 71 | func getZoteroDirectories() ([]ZoteroDirectory, error) { 72 | var directoriesJson []ZoteroDirectory 73 | _, err := getJson(BaseZoteroURL+UserId+"/collections", &directoriesJson) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return directoriesJson, nil 78 | } 79 | 80 | func getZoteroItemsForDirectory(directory ZoteroDirectory) ([]ZoteroItem, error) { 81 | var zoteroItems []ZoteroItem 82 | fmt.Println(BaseZoteroURL + UserId + "/collections/" + directory.Key + "/items") 83 | res, err := getJson(BaseZoteroURL+UserId+"/collections/"+directory.Key+"/items", &zoteroItems) 84 | if err != nil { 85 | return nil, err 86 | } 87 | next := "" 88 | if val, ok := link.ParseResponse(res)["next"]; ok { 89 | next = val.String() 90 | } 91 | for next != "" { 92 | var tempZoteroItems []ZoteroItem 93 | res, err := getJson(next, &tempZoteroItems) 94 | if err != nil { 95 | return nil, err 96 | } 97 | zoteroItems = append(zoteroItems, tempZoteroItems...) 98 | next = "" 99 | if val, ok := link.ParseResponse(res)["next"]; ok { 100 | next = val.String() 101 | } 102 | } 103 | 104 | return zoteroItems, nil 105 | } 106 | 107 | func getZoteroPdfsFromItems(items []ZoteroItem) []ZoteroItem { 108 | var zoteroPdfs []ZoteroItem 109 | for _, item := range items { 110 | if item.Data.ContentType == "application/pdf" { 111 | zoteroPdfs = append(zoteroPdfs, item) 112 | } 113 | } 114 | return zoteroPdfs 115 | } 116 | 117 | func createRemarkableFileMap(files []rmsync.RemarkableFile) map[string]struct{} { 118 | fileMap := make(map[string]struct{}, 0) 119 | for _, file := range files { 120 | fileMap[file.VisibleName] = struct{}{} 121 | } 122 | return fileMap 123 | } 124 | 125 | func getSharedDirectories(directories []rmsync.RemarkableFile, zoteroDirectories []ZoteroDirectory) []ZoteroDirectory { 126 | var sharedDirectories []ZoteroDirectory 127 | dirMap := createRemarkableFileMap(directories) 128 | for _, zoteroDirectory := range zoteroDirectories { 129 | if _, ok := dirMap[zoteroDirectory.Data.Name]; ok { 130 | sharedDirectories = append(sharedDirectories, zoteroDirectory) 131 | } 132 | } 133 | return sharedDirectories 134 | } 135 | 136 | func createRemarkableFilesToSync(pdfs []ZoteroItem) []rmsync.FileToSync { 137 | files := make([]rmsync.FileToSync, 0) 138 | for _, pdf := range pdfs { 139 | files = append(files, rmsync.FileToSync{pdf.Data.Filename, pdf.Data.Url}) 140 | } 141 | return files 142 | } 143 | 144 | func sync() error { 145 | directories, err := rmsync.GetDirectoriesMetadataFiles() 146 | if err != nil { 147 | return err 148 | } 149 | zoteroDirectories, err := getZoteroDirectories() 150 | if err != nil { 151 | return err 152 | } 153 | sharedDirectories := getSharedDirectories(directories, zoteroDirectories) 154 | for _, zoteroDirectory := range sharedDirectories { 155 | items, err := getZoteroItemsForDirectory(zoteroDirectory) 156 | if err != nil { 157 | return err 158 | } 159 | pdfs := getZoteroPdfsFromItems(items) 160 | filesToSync := createRemarkableFilesToSync(pdfs) 161 | err = rmsync.Sync(filesToSync) 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | var prevSyncedTime time.Time 170 | 171 | func main() { 172 | for { 173 | currTime := time.Now() 174 | if prevSyncedTime.IsZero() { 175 | err := sync() 176 | if err != nil { 177 | fmt.Println(err) 178 | continue 179 | } 180 | prevSyncedTime = currTime 181 | fmt.Println("SUCCESSFUL SYNC") 182 | continue 183 | } 184 | diff := currTime.Sub(prevSyncedTime) 185 | 186 | if diff > time.Minute*10 { 187 | err := sync() 188 | if err != nil { 189 | fmt.Println(err) 190 | continue 191 | } 192 | prevSyncedTime = currTime 193 | fmt.Println("SUCCESSFUL SYNC") 194 | } 195 | time.Sleep(time.Minute) 196 | } 197 | } 198 | --------------------------------------------------------------------------------