GithubHelp home page GithubHelp logo

mholt / timeliner Goto Github PK

View Code? Open in Web Editor NEW
3.6K 60.0 116.0 272 KB

All your digital life on a single timeline, stored locally -- DEPRECATED, SEE TIMELINIZE (link below)

Home Page: https://timelinize.com

License: GNU Affero General Public License v3.0

Go 100.00%
backup google-photos twitter facebook google-location-history instagram oauth2 google-takeout

timeliner's Introduction

Note

Timeliner is being deprecated in favor of its successor, Timelinize. New development on this repo has stopped.

Don't worry though—Timelinize is a huge improvement:

  • A web UI for viewing your data, finally!
  • CLI and JSON HTTP API for integrations, scripting, and automations.
  • Same overall simple architecture: SQLite DB alongside a folder with your files.
  • Open schema and self-hosted! You always have complete control over and access to your data. No DRM.
  • Many more data sources.
  • Major performance improvements.
  • Bug fixes.

If you are interested in alpha testing Timelinize during the early developer previews, please let me know! I just ask that you be actively involved with our community on Discord during development and offer your feedback/ideas. You can find my email address on the Timelinize website.

Timeliner ➡️ Timelinize FAQ

Where is it? What is the status of Timelinize?

Timelinize is still undergoing heavy development and is not yet release-worthy, but I have alpha versions that developers can preview if you wish. You can request access in an issue or via email. Timelinize has a basic website now. The email address to reach me at is on that site.

You may wonder, why are there not more screenshots!? A valid question. I would LOVE to show you but all the data I have to develop with is my own personal data, of course! It's hard to find parts of it that are not sensitive. I'm working on ways to either generate fake data or obfuscate real data for demo purposes. Then there will be more screenshots.

Why a new project?

Timeliner was originally a project called Photobak, for backing up your photos from Picasa (then Google Photos). Then I realized that I wanted to back up all the photos I put on Facebook, Twitter, and Instagram. And while I was at it, why not my Google Location History? The project evolved enough to warrant a new name, hence Timeliner. Well, the next evolution is here, and I feel like it finally starts to live up to the vision I originally had after Photobak: a complete, highly detailed archive of my digital content and life and that of my family's. The overall scope changed enough that it warranted a new name.

Perhaps the biggest reason is the development of a UI, which hopefully makes the project accessible to many more people like my own family members. There are also technical reasons. Over the years I've found that Timeliner's method of ingesting data via API downloads is brittle, as services cut off free API access (see Twitter/X), or strip critical data when using APIs (see Google Photos). This necessitated hacking in alternate ways of augmenting one's timeline, e.g. importing a Google Takeout to replace the missing data from Google Photos' API. This is tedious and inelegant. Learning from lessons like this, I've redesigned Timelinize to be more flexible going forward.

Will Timelinize be open source?

Undecided. The schema is for sure going to be open. The project has taken an exceptional amount of my time over the last decade, and that cost may be too high for me if everyone uses it for free. Because self-hosting is one of the primary values of this project, and I don't even want to host your private data, selling a hosted version of the app as a way to make money isn't a favorable option. If Timelinize doesn't end up open source, it will likely have a very generous free tier.

Will Timelinize send any of my data to someone's servers or store it anywhere else?

NO. Just like Timeliner, all your data is stored on your own computer and it doesn't go through any remote servers when you use Timelinize. You can verify this with tools like Wireshark. In the future, we have plans to implement sharing features so you can securely choose parts of your data/timeline to share with peers like your friends and family. I'm hoping that through technologies like Wireguard, Tailscale, or OpenZiti/zrok, we can implement those sharing features directly P2P, and the only interactions with our servers would be to coordinate the sharing and permissions.

Can Timelinize do everything Timeliner does?

Fundamentally, yes. The result is achieved differently though; in particular, API downloads have been shelved for now. To explain: Timeliner downloads data from APIs. Unfortunately, we've seen in the last several years that API offerings are brittle. Twitter/X cut off all free access, even to your own data; Google Photos strips location data from your own photos when downloaded via the API; etc. So while Timelinize has the ability to do API downloads, its primary method of ingesting data for now is importing files on disk, usually obtained from services' "takeout" or "export" services such as like Google Takeout or "Download your account data" pages, which has to be done manually. So, Timelinize is a little less "set it and forget it," but you will likely only need to do major imports about once every year or so. And I'm experimenting with ways to automate those, too.

How different is Timelinize?

Well, the fact that it has a graphical UI makes it hugely different. But it still feels familiar. It has the same basic architecture: you import data that gets stored in a SQLite DB on your computer, except for binary data that gets stored in a folder adjacent to it. Everything is indexed and searchable. It's organized primarily by time. The main difference is it's much more capable.

What are some improvements that Timelinize makes over Timeliner?

The schema has been greatly improved. Instead of focusing only on items as data (with an associated "person" for each item), Timelinize is more fully "entity-aware." An entity is a person, place, pet/animal, organization, etc; and each entity can have 1 or more defining attributes. An attribute may be like an email or phone number, account ID or username. Some attributes are identifying. This enables us to represent the same entity using multiple identifiers, which is useful when crossing data sources. You'll find that we can automatically connect the Sally Jane on your email, for example, as the same Sally Jane in your text messages, even if her display name is different (and if we don't, merging two duplicate entities is a cinch)! Attributes can also map to multiple entities, for example if two people share an email address, we can represent that.

Another schema improvement is the representation of items. They now have multiple time fields: timestamp, timespan, timeframe, time_offset, and time_uncertainty. Combining these allows us to represent items that span time, have an uncertain time, have different time zones, or take place at an unknown time within some timeframe. (This is useful for location data when traveling, importing scans of old photos, etc.) We also store the original path or location of each item as well as its intermediate path or location. For example, items imported from iPhones have a location as originally found on the actual iPhone; they have a different path in the iPhone backup the data was imported from. We preserve both now so you can better trace back an item's origins.

Timelinize imports data much faster as well.

How can I view my data using Timelinize?

The web UI will launch with at least 6 ways to view your data:

  1. Timeline view
  2. Map view
  3. Conversation view
  4. Gallery view
  5. Raw item list
  6. Raw entity list

The first four are "projections" of your data into a certain paradigm. A possible future projection may be a calendar. I'm sure we'll think of more, too.

Is Timelinize a rewrite?

Technically yes, as I started with an empty main(). However, I brought over a lot of the code from Timeliner file-by-file. However... I ended up changing a lot of it, and completely rewrote the import logic into an all-new pipeline to improve performance and correctness. So the fundamental code concepts are still mostly intact (ItemGraph, Person, PersonAttribute, etc. -- though they have names like Graph, Entity, and Attribute now). I would say, "Much of it has been rewritten."

What is different about the CLI?

The Timelinize web UI uses a JSON HTTP API for its functionality. That same API is available for you to use, and from that, we also auto-generate a CLI. That means you can completely operate Timelinize through its CLI as much as its GUI and its API. Pretty cool! But yes, Timelinize is very much a breaking change over Timeliner.

Will I be able to port my Timeliner repo to a Timelinize repo?

No, but that's actually a good thing, since Timelinize timelines are much more capable and detailed. Even if I did write code to port Timeliner data to Timelinize, you'd lose the magic of what Timelinize can offer.

Do you have an ETA?

I do not have a timeline. The irony of this is not lost on me.

Original docs (DEPRECATED) timeliner godoc

Timeliner is a personal data aggregation utility. It collects all your digital things from pretty much anywhere and stores them on your own computer, indexes them, and projects them onto a single, unified timeline.

The intended purpose of this tool is to help preserve personal and family history.

Things that are stored by Timeliner are easily accessible in a SQLite database or, for media items like photos and videos, are simply plopped onto your disk as regular files, organized in folders by date and data source.

WIP Notice: This project works as documented for the most part, but is still very young. Please consider this experimental until stable releases. The documentation needs a lot of work too, I know... so feel free to contribute!

About

In general, Timeliner obtains items from data sources and stores them in a timeline.

  • Items are anything that has content: text, image, video, etc. For example: photos, tweets, social media posts, even locations.
  • Data sources are anything that can provide a list of items. For example: social media sites, online services, archive files, etc.
  • Timelines are repositories that store the data. Typically, you will have one timeline that is your own, but timelines can support multiple people and multiple accounts per person if you desire to share it.

Technically speaking:

  • An Item implements this interface and provides access to its content and metadata.
  • A DataSource is defined by this struct which configures a Client to access it (by its NewClient field). Clients are the types that do the actual work of listing of items.
  • A Timeline is opened when being used. It consists of an underlying SQLite database and an adjacent data folder where larger/media items are stored as files. Timelines are essentially the folder that contains them. They are portable, so you can move them around and won't break things. However, don't change the contents of the folder directly! Don't add, remove, or modify items in the folder; you will break something. This does not mean timelines are read-only: they just have to be modified through the program in order to stay consistent.

Timeliner can pull data in from local or remote sources. It provides integrated support for OAuth2 and rate limiting where that is needed. It can also import data from local files. For example, some online services let you download all your data as an archive file. Timeliner can read those and index your data.

Timeliner data sources are strictly read-only meaning that no write permissions are needed and Timeliner will never change or delete from the source.

Features

  • Supported data sources
  • Checkpointing (resume interrupted downloads)
  • Pruning
  • Integrity checks
  • Deduplication
  • Timeframing
  • Differential reprocessing (only re-process items that have changed on the source)
  • Construct graph-like relationships between items and people
  • Memory-efficient for high-volume data processing
  • Built-in rate limiting for API clients
  • Built-in OAuth2 facilities for API clients
  • Configurable data merging behavior for similar/identical items
  • Ability to get and organize data from... almost anything, really, including export files

Some features are dependent upon the actual implementation of each data source. For example, differential reprocessing requires that the data source provide some sort of checksum or "ETag" for the item, but if that is not available, there's no way to know if an item has changed remotely without downloading the whole thing and reprocessing it.

Install

Minimum Go version required: Go 1.13

Clone this repository, then from the project folder, run:

$ cd cmd/timeliner
$ go build

Then move the resulting executable into your PATH.

Command line interface

This is a quick reference only. Be sure to read the tutorial below to learn how to use the program!

$ timeliner [<flags...>] <command> <args...>

Use timeliner -h to see available flags.

Commands

  • add-account adds a new account to the timeline and, if relevant, authenticates with the data source so that items can be obtained from an API. This only has to be done once per account per data source:
     $ timeliner add-account <data_source>/<username>...
    
    If the data source requires authentication (for example with OAuth), be sure the config file is properly created first.
  • reauth re-authenticates with a data source. This is only necessary on some data sources that expire auth leases after some time:
     $ timeliner reauth <data_source>/<username>...
    
  • import adds items from a local file:
     $ timeliner import <filename> <data_source>/<username>
    
  • get-all adds items from the service's API.
     $ timeliner get-all <data_source>/<username>...
    
  • get-latest adds only the latest items from the service's API (since the last checkpoint):
     $ timeliner get-latest <data_source>/<username>...
    

Flags can be used to constrain or customize the behavior of commands (timeliner -h to list flags).

See the wiki page for your data sources to know how to use the various data sources.

Tutorial

After you've read this tutorial, the Timeliner wiki has all the information you'll need for using each data source.

These are the basic steps for getting set up:

  1. Create a timeliner.toml config file (if any data sources require authentication)
  2. Add your data source accounts
  3. Fill your timeline

All items are associated with an account from whence they come. Even if a data source doesn't have the concept of accounts, Timeliner still has to think there is one.

Accounts are designated in the form <data source ID>/<user ID>, for example: twitter/mholt6. The data source ID is shown on each data source's wiki page. With some data sources (like the Twitter API), the user ID matters; so where possible, give the actual username or email address you use with that service. For data sources that don't have the concept of accounts or a login, choose a user ID you will recognize such that the data source ID + user ID are unique.

If we want to use accounts that require OAuth2, we need to configure Timeliner with OAuth2 app credentials. You can learn which data sources need OAuth2 and what their configuration looks like by reading their wiki page. By default, Timeliner will try to load timeliner.toml from the current directory, but you can use the -config flag to change that. Here's a sample timeliner.toml file for authenticating with Google:

[oauth2.providers.google]
client_id = "YOUR_APP_ID"
client_secret = "YOUR_APP_SECRET"
auth_url = "https://accounts.google.com/o/oauth2/auth"
token_url = "https://accounts.google.com/o/oauth2/token"

With that file in place, let's create an account to store our Google Photos:

$ timeliner add-account google_photos/[email protected]

This will open your browser window to authenticate with OAuth2.

You will notice that a folder called timeliner_repo was created in the current directory. This is your timeline. You can move it around if you want, and then use the -repo flag to work with that timeline.

Now let's get all our stuff from Google Photos. And I mean, all of it. It's ours, after all:

$ timeliner get-all google_photos/[email protected]

(You can list multiple accounts on a single command, except import commands.)

This process can take weeks if you have a large library. Even if you have a fast Internet connection, the client is carefully rate-limited to be a good API citizen, so the process will be slow.

If you open your timeline folder in a file browser, you will see it start to fill up with your photos from Google Photos. To see more verbose logging, use the -v flag (NOTE: this will drastically slow down processing that isn't bottlenecked by the network).

Data sources may create checkpoints as they go. If so, get-all or get-latest will automatically resume the last listing if it was interrupted, but only if the same command is repeated (you can't resume a get-latest with get-all, for example, or with different timeframe parameters). In the case of Google Photos, each page of API results is checkpointed. Checkpoints are not intended for long-term pauses. In other words, a resume should happen fairly shortly after being interrupted, and should be resumed using the same command as before. (A checkpoint will be automatically resumed only if the command parameters are identical.)

Item processing is idempotent, so as long as items have faithfully-unique IDs from their account, items that already exist in the timeline will be skipped and/or processed much faster.

Constraining within a timeframe

You can use the -start and -end flags to specify either absolute dates within which to constrain data collection, or with duration values to specify a date relative to the current timestamp. These flags appear before the subcommand.

To get all the items newer than a certain date:

$ timeliner -start=2019/07/1 get-all ...

This will get all items dated July 1, 2019 or newer.

To get all items older than certain date:

$ timeliner -end=2020/02/29 get-all ...

This processes all items before February 29, 2020.

To create a bounded window, use both:

$ timeliner -start=2019/07/01 -end=2020/02/29 get-all ...

Durations can be used for relative dates. To get all items up to 30 days old:

$ timeliner -end=-720h get-all ...

Notice how the duration value is negative; this is because you want the end date to be 720 hours (30 days) in the past, not in the future.

Pulling the latest

Once your initial download completes, you can run Timeliner so that only the latest items are retrieved:

$ timeliner get-latest google_photos/[email protected]

This will get only the items timestamped newer than the newest item in your timeline (from the last successful run).

If get-latest is interrupted after adding some newer items to the timeline, the next run of get-latest will not stop at the first new item added last time; it is smart enough to know that it was interrupted and needs to keep getting items all the way until the beginning of the last successful run, as long as the command's parameters are the same. For example, re-running the last command will automatically resume where it left off; but changing the -end flag, for example, won't be able to resume.

This subcommand supports the -end flag, but not the -start flag (since the start is determined from the last downloaded item). One thing I like to do is use -end=-720h with my Google Photos to only download the latest photos that are at least 30 days old. This gives me a month to delete unwanted/duplicate photos from my cloud library before I store them on my computer permanently.

Duplicate items

Timeliner often encounters the same items multiple times. By default, it skips items with the same ID as one already stored in the timeline because it is faster and more efficient, but you can also configure it to "reprocess" or "merge" duplicate items. These two concepts are distinct and important.

Reprocessing is when Timeliner completely replaces an existing item with a new one.

Merging is when Timeliner combines a new item's data with an existing item.

Neither happen by default because they can be less efficient or cause undesired results. In other words: by default, Timeliner will only download and process and item once. This makes its get-all, get-latest, and import commands idempotent.

Reprocessing

Reprocessing replaces items with the same ID. This happens if one of the following conditions is met:

  • You run with the -integrity flag which enables integrity checks, and an item's data file fails the integrity check. In that case, the item will be reprocessed to restore its correct data.

  • The item has changed on the data source and the data source indicates this change somehow. However, very few (if any?) data sources actually provide a hash or ETag to help us compare whether a resource has changed.

  • You run with the -reprocess flag. This does a "full reprocess" (or "forced reprocess") which indiscriminately reprocesses every item, just in case it changed. In other words, a forced reprocess will update your local copy with the source's latest for every item. This is often used because a data source might not provide enough information to automatically determine whether an item has changed. If you know you have changed your items on the data source, you could specify this flag to force Timeliner to update everything.

Merging

Merging combines two items without completely replacing the old item. Merges are additive: they'll never replace a field with a null value. By default, merges only add data that was missing and will not overwrite existing data (but this is configurable).

In theory, any two items can be merged, even if they don't have the same ID. Currently, the only way to trigger a merge is to enable "soft merging" which allows Timeliner to treat two items with different IDs as identical if ALL of these are true:

  • They have the same account (same data source)
  • They have the same timestamp
  • They have either the same text data OR the same data file name

Merging can be enabled and customized with the -merge flag. This flag accepts a comma-separated list of merge options:

  • soft (required): Enables soft merging. Currently, this is the only way to enable merging at all.
  • id: Prefer new item's ID
  • text: Prefer new item's text data
  • file: Prefer new item's data file
  • meta: Prefer new item's metadata

Soft merging simply updates the ID of either the existing, stored item or the new, incoming item to be the same as the other. (As with other fields, the ID of the existing item will be preferred by default, meaning the ID of the new item will be adjusted to match it.)

Example: I often use soft merging with Google Photos. Because the Google Photos API strips location data (grrr), I also use Google Takeout to import an archive of my photos. This adds the location data. However, although the archive has coordinate data, it does NOT have IDs like the Google Photos API provides. Thus, soft merging prevents a duplication of my photo library in my timeline.

To illustrate, I schedule this command to run regularly:

$ timeliner -merge=soft,id,meta -end=-720h get-latest google_photos/me

This uses the API to pull the latest photos up to 30 days old so I have time to delete unwanted photos from my library first. Notably, I enable soft merging and prefer the IDs and metadata given by the Google Photos API because they are richer and more precise.

Occasionally I will use Takeout to download an archive to add location data to my timeline, which I import like this:

$ timeliner -merge=soft import takeout.tgz google_photos/me

Note that soft merging is still enabled, but I always prefer existing data when doing this because all I want to do is fill in the missing location data.

This pattern takes advantage of soft merging and allows me to completely back up my Photos library locally, complete with location data, using both the API and Google Takeout.

Pruning your timeline

Suppose you downloaded a bunch of photos with Timeliner that you later deleted from Google Photos. Timeliner can remove those items from your local timeline, too, to save disk space and keep things clean.

To schedule a prune, just run with the -prune flag:

$ timeliner -prune get-all ...

However, this involves doing a complete listing of all the items. Pruning happens at the end. Any items not seen in the listing will be deleted. This also means that a full, uninterrupted listing is required, since resuming from a checkpoint yields an incomplete file listing. Pruning after a resumed listing will result in an error. (There's a TODO to improve this situation -- feel free to contribute! We just need to preserve the item listing along with the checkpoint.)

Beware! If your timeline has extra items added from auxillary sources (for example, using import with an archive file in addition to the regular API pulls), the prune operation may not see those extra items and thus delete them. Always back up your timeline before doing a prune.

Reauthenticating with a data source

Some data sources (Facebook) expire tokens that don't have recent user interactions. Every 2-3 months, you may need to reauthenticate:

$ timeliner reauth facebook/you

See the wiki for each data source to know if you need to reauthenticate and how to do so. Sometimes you have to go to the data source itself and authorize a reauthentication first.

More information about each data source

Congratulations, you've graduated to the wiki pages to learn more about how to set up and use each data source.

Motivation and long-term vision

The motivation for this project is two-fold. Both press upon me with a sense of urgency, which is why I dedicated some nights and weekends to work on this.

  1. Connecting with my family -- both living and deceased -- is important to me and my close relatives. But I wish we had more insights into the lives and values of those who came before us. What better time than right now to start collecting personal histories from all available sources and develop a rich timeline of our life for our family, and maybe even for our own reference or nostalgia.

  2. Our lives are better-documented than any before us, but the documentation is more ephemeral than any before us, too. We lose control of our data by relying on centralized, proprietary cloud services which are useful today, and gone tomorrow. I wrote Timeliner because now is the time to liberate my data from corporations who don't own it, yet who have the only copy of it. This reality has made me feel uneasy for years, and it's not going away soon. Timeliner makes it bearable.

Imagine being able to pull up a single screen with your data from any and all of your online accounts and services -- while offline. And there you see so many aspects of your life at a glance: your photos and videos, social media posts, locations on a map and how you got there, emails and letters, documents, health and physical activities, and even your GitHub projects (if you're like me), for any given day. You can "zoom out" and get the big picture. Machine learning algorithms could suggest major clusters based on your content to summarize your days, months, or years, and from that, even recommend printing physical memorabilia. It's like a highly-detailed, automated journal, fully in your control, which you can add to in the app: augment it with your own thoughts like a regular journal.

Then cross-reference your own timeline with a global public timeline: see how locations you went to changed over time, or what major news events may have affected you, or what the political/social climate was like at the time.

Or translate the projection sideways, and instead of looking at time cross-sections, look at cross-sections of your timeline by media type: photos, posts, location, sentiment. Look at plots, charts, graphs, of your physical activity.

And all of this runs on your own computer: no one else has access to it, no one else owns it, but you.

Viewing your Timeline

UPDATE: Timelinize is the successor to this project, and it has a fully-featured graphical web UI to view and manage your timeline data!

There is not yet a viewer for the timeline. For now, I've just been using Table Plus to browse the SQLite database, and my file browser to look at the files in it. The important thing is that you have them, at least.

However, a viewer would be really cool. It's something I've been wanting to do but don't have time for right now. Contributions are welcomed along these lines, but this feature must be thoroughly discussed before any pull requests will be accepted to implement a timeline viewer. Thanks!

Notes

Yeah, I know this is very similar to what Perkeep does. Perkeep is a way cooler project in my opinion. However, Perkeep is more about storage and sync, whereas Timeliner is more focused on constructing relationships between items and projecting your digital life onto a single timeline. If Perkeep is my unified personal data storage, then Timeliner is my automatic journal. (Believe me, my heart sank after I realized that I was almost rewriting parts of Perkeep, until I decided that the two are different enough to warrant a separate project.)

License

This project is licensed with AGPL. I chose this license because I do not want others to make proprietary software using this package. The point of this project is liberation of and control over one's own, personal data, and I want to ensure that this project won't be used in anything that would perpetuate the walled garden dilemma we already face today. Even if this project's official successor has proprietary source code, I can ensure it will stay aligned with my values and the project's original goals.

timeliner's People

Contributors

ayumukasuga avatar fawick avatar joonas-fi avatar mholt avatar yangsu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

timeliner's Issues

Add Gmail data source

1. What is the data source you want to add?

Gmail - the ability to download emails and attachments, with very heavy filtering options. Having a record of emails would be useful because my family often documents their lives to the rest of us through emails.

2. How are items obtained from the data source?

Gmail API via Google OAuth2

2a. If authentication is required, how does a user create or obtain credentials for Timeliner to access the data?

Google OAuth2 is already documented in the wiki.

2b. If an API is available, what are its rate limits?

250 queries per user per second: https://developers.google.com/gmail/api/v1/reference/quota

2c. If a file is imported, how is the file obtained?

n/a

3. What constitutes an "item" from this data source?

An email or attachment.

4. How can items from this data source be related?

Emails can have attachments. Emails can be replies to other emails.

5. What constitutes a "collection" from this data source?

Perhaps a conversation (a thread of messages); but that might be redundant if emails can be related as replies to other ones.

6. What might not be trivial, obvious, or straightforward when implementing this data source?

A lot of filtering will need to be configurable, since Gmail contains a messy amount of noise; somehow I have Google Voice messages, Hangout chats, and other things that aren't really emails in my Gmail account which also show up in the API.

Will need to filter by label, subject, sender, To, other headers, etc. Optionally include attachments (from some emails only, or files of a certain type or size, etc).

For Windows users: Add a note about the gcc dependency to the README

Hi, I just tried installing timeliner on Windows 10 and noticed that the go-sqlite3 dependency requires gcc.

On Windows, gcc is not included and has to be installed manually. Maybe you could add a note about that in the install section of the readme, as long as there will be no binaries available for download.

Using chocolatey, this installs the necessary dependencies on Windows:

choco install golang mingw

Add in-progress messaging / Verbose output

Currently when I'm either downloading from sources or importing files, very little output is written to the screen. I'd love it if I can see some kind of progress being made or some kind of logging information being printed. Even if I have to add a flag to get it.

Finish Twitter API data source

I've started this, but pulling content from the Twitter API does not currently add relationships between tweets (like replies).

Twitter: aborts if media download yields "403 Forbidden", e.g. removed by copyright claim

Here's the Tweet: https://twitter.com/janl/status/1113015555064201216

Error message:

2019/11/30 18:04:02 [ERROR][twitter/joonas_fi] Getting latest: getting items from service: processing tweet from API: processing tweet 1113180316510957568: making item from tweet that this tweet (1113180316510957568) is in reply to (1113015555064201216): making item from tweet that this tweet (1113015555064201216) embeds (1112473455650172929): media resource returned HTTP status 403 Forbidden: https://pbs.twimg.com/ext_tw_video_thumb/1112471832232259585/pu/img/ywWGTl09hsnLnMOY.jpg

That image URL redirects (when used with browser - different when API use?) to this DMCA warning.

Timeliner cannot cope with this, and trying to re-run Timeliner always gets me this and cannot continue.

Stuck on authorization screen

I put in the command to setup google photos. It pulls up the browser to login. I get all the way to where I'm confirming my choices to allow my app access. Then it just churns with a "waiting for localhost" in the bottom left. Am I just not patient enough? Does this take a long time?

Amazon data source

Excellent work with timeliner Matt :)

Can I suggest an Amazon data source? Specifically things like order history etc. Or is that out of scope?

Authenticating via CLI

I'm trying to use this directly on my NAS (a Synology DS218j), so I'm runnig it via Linux command line with no browser access.

I have timeliner installed and ready to run, but I can't authenticate as I can't open a browser within command line.

Is there a way to get around that and authenticate via the command line?

Facebook data import data source

1. What is the data source you want to add?

Facebook data exports. Currently there's a date source for data from the API, but a Timeliner user might have already closed their account, leaving behind only their data.

2. How are items obtained from the data source?

Data must be manually imported. It may be sourced from some or all of Facebook's different offerings. Documentation is here.

2a. If authentication is required, how does a user create or obtain credentials for Timeliner to access the data?

N/A

2b. If an API is available, what are its rate limits?

N/A

2c. If a file is imported, how is the file obtained?

Documentation here.

2d. If a file is imported, how do we read the file?

Exports are a ZIP file containing a structure of folders, JSON files, and various media files. The media files have their EXIF data, but additional metadata from Facebook corresponding to each one is in the associated JSON file.

3. What constitutes an "item" from this data source?

It depends how much of the export is considered useful, but photos, posts, milestones etc. should all be considered.

4. How can items from this data source be related?

This should be looked into more closely by someone who actually uses Facebook – my memory for it isn't great as I type this.

5. What constitutes a "collection" from this data source?

Facebook has the concept of photo albums, so they'd constitute a collection. Other than that, I doubt there are many others.

6. What might not be trivial, obvious, or straightforward when implementing this data source?

I'm not sure how easy it would be to make the relationship between data from the JSON files and actual media files themselves clearer.

Bonus: How do you like Timeliner? How much data are you preserving with it? Which existing data sources do you use?

I haven't started using it yet, but I figure a Facebook data export would serve as the foundation for my starting to use this on my VPS.

I'm closing my Facebook account as I type this, but it'd be good to hang on to the backed-up data in a way I might be able to browse in future. I've observed from the wiki that there's not yet support for loading from a full backup taken from Facebook (whether HTML or JSON). I'd hope there's some support for this thanks to this issue.

Cannot find package "github.com/pierrec/lz4/v3" during install

Hey guys,

I'm getting the following error that prevents me from installing timeliner:

go get -u github.com/mholt/timeliner/cmd/timeliner
package github.com/pierrec/lz4/v3: cannot find package "github.com/pierrec/lz4/v3" in any of:
        /usr/lib/go-1.10/src/github.com/pierrec/lz4/v3 (from $GOROOT)
        /home/ngirardin/go/src/github.com/pierrec/lz4/v3 (from $GOPATH)

Best,

Nicolas

parsing width as int: strconv.Atoi: parsing "": invalid syntax (width=)

I get the following error message, and timeliner consistently doesn't download all pictures

2019/05/29 05:02:01 [ERROR][google_photos/xxx@xxx] Processing item graph: processing node of item graph: assembling item for storage: getting item metadata: parsing width as int: strconv.Atoi: parsing "": invalid syntax (width=)

operation not supported and disk I/O error

OS: macOS (latest)
GO: go version go1.12.6 darwin/amd64

I am getting this error. Everything is done as per README.md

2019/06/16 19:58:44 [ERROR][google_photos/[email protected]] Processing item graph: processing node of item graph: downloading data file: syncing file: sync timeliner_repo/data/2019/06/google_photos/IMG_5773.JPG: operation not supported (item_id=6)

Another error:

2019/06/16 20:00:21 [ERROR][google_photos/[email protected]] Processing item graph: processing node of item graph: storing item in database: disk I/O error (item_id=XXXXXXX)

Question : how does it handle duplicates?

I couldn't find in the documentation, how does timeliner handle dupliates?
let say if I have same photo on google photos and facebook, when it downloads it, can it detect duplicates or warn me?

or does it automatically keep one of them ?
what happens in case of date conflict ? like one of them has a wrong date and one of them correct date.

Include videos

Someone correct me if I'm wrong but it appears videos are excluded in the get-all command.
It would be nice if they were included as well.

Add Apple data source

In Europe, a personal data archive can be downloaded from https://privacy.apple.com/

Your download will include:

    App usage and activity information as spreadsheets or files in JSON, CSV, XML or PDF format.
    Documents, photos and videos in their original format.
    Contacts, calendars and bookmarks in VCF, ICS or HTML format.

Your download will not include app, book, film, TV programme or music purchases.

Detailed list of apps and services:


    Apple Media Services information
    Apple ID account and device information
    Apple Online Store and Retail Store activity
    Apple Pay activity
    AppleCare support history, repair requests and more
    Game Center activity
    iCloud Bookmarks and Reading List
    iCloud Calendars and Reminders
    iCloud Contacts
    iCloud Drive files and documents
    iCloud Mail
    iCloud Notes
    iCloud Photos
    Maps Report an Issue
    Marketing communications, downloads and other activity
    Other data

Add test data/db

Test data and db with all available features used (images, videos, posts, relationships etc) would be useful for development of an interface as it would allow for development of features even if you don't have your own data with all features.

Add Telegram data source

The Telegram desktop app has an option "Export Telegram data" as a json (or html) file including chats, contacts, media and a whole lot more...

I will try to integrate it if I find some time ;)

Unable to add twitter data source

Currently on go 1.13.5

root@778c41ca01ec:~/timeliner/cmd/timeliner# ./timeliner add-account twitter/pfrcks                  
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7c363f]

goroutine 1 [running]:
github.com/mholt/timeliner.authorizeWithOAuth2(0xb24986, 0x7, 0x0, 0x0, 0x0, 0x80, 0xa9a0c0, 0xc000130f01, 0xc0000f6e00, 0xc000130f70)
        /root/timeliner/oauth2.go:57 +0x5f
github.com/mholt/timeliner.DataSource.authFunc.func1(0x7ffcf4a2cee9, 0x6, 0xb246df, 0x7, 0xb24986, 0x7, 0x0)
        /root/timeliner/datasource.go:116 +0x5e
github.com/mholt/timeliner.(*Timeline).Authenticate(0xc0001e77c0, 0x7ffcf4a2cee1, 0x7, 0x7ffcf4a2cee9, 0x6, 0x0, 0xc0001e77e0)
        /root/timeliner/account.go:79 +0x582
github.com/mholt/timeliner.(*Timeline).AddAccount(0xc0001e77c0, 0x7ffcf4a2cee1, 0x7, 0x7ffcf4a2cee9, 0x6, 0x1, 0x0)
        /root/timeliner/account.go:63 +0x2e5
main.main()
        /root/timeliner/cmd/timeliner/main.go:94 +0x554
root@778c41ca01ec:~/timeliner/cmd/timeliner# 

error replacing data file database locked

Thanks for this great tool.

Any idea what must be provoking this error:
Processing item graph: processing node of item graph: replacing data file with identical existing file: querying DB: database is locked

Consider using Metabase over TablePlus

I haven't tested your project yet, but it seems awesome. I'd like to suggest you to consider using Metabase https://www.metabase.com/ over TablePlus. The installation process is very simple and I think this project could benefit a lot from a visualization tool such as Metabase.

Google plus

Hello Google plus is shutting down end of march 2019. Is that possible to include it ?

invalid grant from Google Photos

On trying to use timeliner with Google Photos I am constantly getting a Bad Request error as follows:

 Get https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=: oauth2: cannot fetch token: 400 Bad Request
Response: {
  "error": "invalid_grant",
  "error_description": "Bad Request"
} <<< - retrying... (attempt 5/10)

Any ideas?

Document database structure

Documenting how the database is structured could be useful for starting work on an interface. I'm looking at it and without some digging it's hard to understand what some of the stuff is. For example the "class" for "items" is, I assume based on this however it's not clear

timeliner/itemgraph.go

Lines 134 to 144 in 3284be8

// Various classes of items.
const (
ClassUnknown ItemClass = iota
ClassImage
ClassVideo
ClassAudio
ClassPost
ClassLocation
ClassEmail
ClassPrivateMessage
)

panic: runtime error: invalid memory address or nil pointer dereference on master

➜ ./timeliner add-account google_photos/[email protected]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7439b0]

goroutine 1 [running]:
github.com/mholt/timeliner.authorizeWithOAuth2(0x9d6631, 0x6, 0xe35f60, 0x1, 0x1, 0x7405bc, 0x80, 0x95bb40, 0x4000126f28, 0x7405f8)
/home/sainyam/go/src/github.com/mholt/timeliner/oauth2.go:57 +0x50
github.com/mholt/timeliner.DataSource.authFunc.func1(0x7fdc21d5fa, 0x20, 0x9d8bab, 0xd, 0x9d6631, 0x6, 0xe35f60)
/home/sainyam/go/src/github.com/mholt/timeliner/datasource.go:116 +0x68
github.com/mholt/timeliner.(*Timeline).Authenticate(0x400000f0a0, 0x7fdc21d5ec, 0xd, 0x7fdc21d5fa, 0x20, 0x0, 0x400000f0c0)
/home/sainyam/go/src/github.com/mholt/timeliner/account.go:79 +0x4a8
github.com/mholt/timeliner.(*Timeline).AddAccount(0x400000f0a0, 0x7fdc21d5ec, 0xd, 0x7fdc21d5fa, 0x20, 0x1, 0x0)
/home/sainyam/go/src/github.com/mholt/timeliner/account.go:63 +0x2f0
main.main()
/home/sainyam/go/src/github.com/mholt/timeliner/cmd/timeliner/main.go:91 +0x48c

This is happening at the first stage,

Interface design/type

Type

Should it have a web interface or a desktop one? Maybe both? Possibly add an API to make it easier to create different types of interfaces. Should it be available from other devices? For example through the web or a phone app. Should import be available through the interface and if you have "external" interfaces (phone, web etc) should you be able to import it from there?

I think an API and a local only interface for import. Import (and other modification) shouldn't be available on other devices unless password protection is added.

Design (for a graphical interface)

A timeline sounds like a good idea (based of name). Should you be able to sort it by type (video, audio, image, place etc)? Separate more detailed views for each month while only providing a overview on the timeline?

I think a timeline with general month views and then a more detailed monthly view would be good, since it's supposed to be a long term collection then a general yearly view might be good too.

Any more suggestions?

File Writer Interface

First of all thank you @mholt for this tool. I was looking at the source code to implement my own file writer so that I can save files in remote location ex. perkeep, s3 and others. How hard do you think is to abstract that part?
Thanks.

invalid memory address or nil pointer dereference

I get this error when attempting ./timeliner add-account google_photos/[email protected]

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4367335]

goroutine 1 [running]:
github.com/mholt/timeliner.authorizeWithOAuth2(0x463ed5a, 0x6, 0x4a16410, 0x1, 0x1, 0x4363f34, 0x45cde60, 0xc000100200, 0x0, 0x0)
	/Users/craig/go/src/github.com/mholt/timeliner/oauth2.go:57 +0x65
github.com/mholt/timeliner.DataSource.authFunc.func1(0x7ffeefbffade, 0x18, 0x4641041, 0xd, 0x463ed5a, 0x6, 0x4a16410)
	/Users/craig/go/src/github.com/mholt/timeliner/datasource.go:116 +0x3c
github.com/mholt/timeliner.(*Timeline).AddAccount(0xc00000d1a0, 0x7ffeefbffad0, 0xd, 0x7ffeefbffade, 0x18, 0x1, 0x0)
	/Users/craig/go/src/github.com/mholt/timeliner/account.go:69 +0x754
main.main()
	/Users/craig/go/src/github.com/mholt/timeliner/cmd/timeliner/main.go:82 +0x3b9

Add flags to readme

I suggest documenting the flags, and their usage, in the readme file.
They can be found in main.go or via "timeliner -help"

For example:

timeliner -repo /Volumes/XDrive get-all google_photos/[email protected]

oauth token expired

hiya,

I'm having a little trouble with oauth tokens.

I've set up timeliner and a google photos data source and managed to start downloading 9GB+.

Now, I receive an error saying the token needs to be refreshed (below)
timeliner

I've tried creating new API keys and removing timerliner from my appl permissions within google but no joy. I haven't even been prompted to re-add app permissions within google.

Any help would be great

Dan

SMS and MMS via "SMS Backup & Restore"

1. What is the data source you want to add?

SMS Backup & Restore (or on Google Play) is the only Android app that I've been satisfied with for backing up my text messages (both SMS and MMS). (I don't want to argue which app is better, or answer why I don't use another app.)

(Sigh, I just found out that it improperly encodes emoji characters. Filed a bug.)

If there is a more standard way of archiving text messages, I'd be willing to use that, but for now, this is the easiest and most straightforward method I could find that works for me.

2. How are items obtained from the data source?

By file import. The app exports all text messages on the device when a backup is performed and stores it locally or on a cloud service. I have mine upload to cloud storage on a weekly basis. It's an XML file that has all the data and metadata that's relevant.

2a. If authentication is required, how does a user create or obtain credentials for Timeliner to access the data?

n/a

2b. If an API is available, what are its rate limits?

n/a

2c. If a file is imported, how is the file obtained?

Transferred from device or downloaded from cloud storage. The app can produce these automatically on a schedule.

2d. If a file is imported, how do we read the file?

There is some (incomplete) documentation here: https://synctech.com.au/sms-backup-restore/fields-in-xml-backup-files/

3. What constitutes an "item" from this data source?

SMS or MMS messages or files.

4. How can items from this data source be related?

Messages can be replies to each other, but this is not really explicit, since SMS and MMS are basically stateless. Most phones just group SMS conversations by phone number ("address") and then thread replies implicitly through their timestamps. MMS conversations have one or more addresses (phone numbers), and all messages with the same set of addresses are implicitly a grouped conversation.

So, relationships of replies can be inferred from timestamp without explicitly saying that one is a reply to another.

5. What constitutes a "collection" from this data source?

Perhaps a conversation between recipients, but again this is implied through the address(es) on the SMS or MMS.

6. What might not be trivial, obvious, or straightforward when implementing this data source?

MMS can be files, SMS is just text. MMS can be just text but might have "rich" text (or emojis) or multiple recipients ("group texts").

SMS and MMS are separate in the data file, but SMS can be related to MMS, and vice-versa. We will probably need to somehow treat them as one list if we want to establish explicit relationships between them.

There's no concept of accounts, but the account ID will need to be the owner's phone number.

All phone numbers (including account ID) will need to be standardized. Recommend E164 format.

A default region/country will need to be available (default to US) if a phone number does not have an explicit country calling code.

Lots of metadata fields are strings with a value of "null" which may need to be explicitly converted to empty or nil string.

[Suggestion] Change metadata datatype in database from blob so some readable format

Currently, the metadata is stored as a blob in the database and it's not straightforward to read and parse from within SQL or other programming languages.

I would suggest to store the metadata in separate tables, but I am not sure what a good design would be for that.

A short-term solution could be to store it as serialized text (e.g. json).

What do you think?

Event Stream

Can this notify you when a data sources data has changed ?
For example when you added a new google photo. Or if you edited it or deleted it.
Its basically an Event Stream.
Alot liek CDC. https://en.wikipedia.org/wiki/Change_data_capture

Not all Data sources would support this i am guessing.

Anyway then the system would be able to see the event and then go get the blob if it wanted to.

just an idea.

Twitter: hitting rate limits - "429 Too Many Requests"

I'm running $ timeliner -twitter-replies -twitter-retweets get-latest twitter/joonas_fi:

2019/11/30 16:36:03 [ERROR][twitter/joonas_fi] Getting latest:
	getting items from service:
	processing tweet from API:
	processing tweet 123:
	making item from tweet that this tweet (123) is in reply to (456):
	making item from tweet that this tweet (456) is in reply to (789):
	making item from tweet that this tweet (789) is in reply to (AAA):
	getting tweet that this tweet (AAA) is in reply to (BBB):
	HTTP error: https://api.twitter.com/1.1/statuses/show.json?id=BBB&tweet_mode=extended:
	429 Too Many Requests

(anonymization and line breaks added by me)

Timeliner configures its Twitter client's rate limit ("with some leeway") to 5 900/h. Bursting is disabled for Twitter, so it's 610ms between requests.

Hidden wrong detours in my thought process

Are all proper requests rate limited?

As you can see, Timeliner is digging some considerable reply chains. My first instinct was "are replies counted against the quota or are only my own tweets counted?". Upon further digging, rate limiter is implemented in http.RoundTripper level for a HTTP client, so that's not the issue. Nice approach BTW, I might use that idea later in my own projects! 👍

Upon making the ezhttp HTTP suggestion in my other PR I remembered there's a plain resp, err := http.Get(mediaURL) call in twitter/twitter.go that bypasses the rate limiting HTTP client. That is used for fetching media, and some (or most) media are fetched from https://pbs.twimg.com/... which is Twitter's domain - are those counted against the quota? Probably not, because if the quotas are tied to the user or the app (API key) and requests for that domain would not count against API quotas..

Does the ratelimiter work properly?

I had a hard time using the ratelimiter standalone, so I just plopped a fmt.Printf(".") in the RoundTrip() and watched the dots appear on the screen as Timeliner chugged along. The dots were appearing calmly so the ratelimiter is working.

What I think is the problem

twitter/api.go use three API endpoints:

Endpoint User limit / 15min App limit / 15min
/users/show.json 900 900
/statuses/show.json 900 900
/statuses/user_timeline.json 900 1500

Source for limits: https://developer.twitter.com/en/docs/basics/rate-limits

Timeliner's ratelimit is shared across all of those endpoints. Timeliner theoretically lets me do 1 475 reqs/15 min to /statuses/show.json - pushing it over the limit of 900. Now I don't know for real what the ratios of the endpoint call rates are, but if we were to avoid going over the limit with this current "all endpoints have same rate limit" -design, the limit should be re-calculated based on the 900 number.

Another thought: is the 1 500 correct even for user_timeline.json?

This depends on the authorization model, if Timeliner:

a) uses only the app's credentials to read public data and doesn't get authorization from the user
b) gets authorization from the user and operates on behalf of the user (I don't remember seeing any authorization screen, but that might be because I had an API key laying around which I had authorized way back)

A few quotes:

Rate limiting of the standard API is primarily on a per-user basis — or more accurately described, per user access token.

When using application-only authentication, rate limits are determined globally for the entire application. If a method allows for 15 requests per rate limit window, then it allows you to make 15 requests per window — on behalf of your application. This limit is considered completely separately from per-user limits.

Source: https://developer.twitter.com/en/docs/basics/rate-limiting

If I understand correctly, Timeliner is not using "When using application-only authentication", so shouldn't the limit be based on the 900 anyway?

I'm not sure of this. WDYT?

Workaround

This is not a serious issue, since after throttling I can wait a while and continue later.

Code suggestions

I don't know if you're interested in code suggestions, but a couple came to mind while kicking the tires:

  1. I came across timeliner.FakeCloser, just in case you're not aware there's a stdlib implementation for that: https://godoc.org/io/ioutil#NopCloser
  2. ratelimit.go: you're chucking empty structs (struct{}) on the token channel. Usually when a channel is only used for signalling, I've seen interface{} used so one can just chuck nil's down the channel. I'm not sure if it's more performant but I think it's more semantic. This might be subjective though.

Need help with installation

I'd like to try Timeliner out, but I'm having difficulties with installing it. I've also never used Go before.

I've spent a lot of time trying and failing under Windows 10 and Ubuntu 18 LTS under WSL. (I haven't found any info re supported platforms. If I'd just missed it, please point me in the right direction.)

I'd installed golang successfully, then tried the go get -u github.com/mholt/timeliner/cmd/timeliner.
At first it threw up errors about missing 'gcc'. I've tried installing MinGW on Windows and one of the suggested packages from apt on Ubuntu.

After that, the Ubuntu go get just ran for a while, then died without printing any messages. Under Windows it threw up an error about a failing SQLite compilation and died as well.

Are these just completely unsupported scenarios? How should I go about running Timeliner?
(I've been writing something similar in the past month, so I'm especially eager to try it out.)

Google Photos (Location)

Hi all,
Just came across this project. In the wiki it mentions that location data is stripped when using the Google Photos API?
I am currently using rclone to pull and backup my Google Photos account to my personal server through Google Drive. All of my media pulled using rclone retains the GPS data (lat/lon/alt).

Two thoughts.

  1. I would rather use my own script (or some other program) to sync my Google Photos account. Would it be easy for timeliner to import from a local folder?
  2. I am not familiar with the Google Photos API, but location data is pretty important (especially if you don't have Location History). It seems alternative methods to import photos with the location data would be of high importance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.