diff --git a/src/LarvaTagger.jl b/src/LarvaTagger.jl
index f17f04602f2ea0754eef56b2b7631eb75847cee5..3f32d4c64d8e6ac545ec565d122ecaec7c2e399e 100644
--- a/src/LarvaTagger.jl
+++ b/src/LarvaTagger.jl
@@ -1,7 +1,6 @@
 module LarvaTagger
 
-using PlanarLarvae, PlanarLarvae.Chore, PlanarLarvae.Trxmat, PlanarLarvae.Datasets,
-      PlanarLarvae.FIMTrack, PlanarLarvae.Loaders
+using PlanarLarvae, PlanarLarvae.Datasets, PlanarLarvae.Loaders
 using ObservationPolicies
 using TidyObservables
 
diff --git a/src/files.jl b/src/files.jl
index 1b0239e984064ca0501bb444e36fd6e75e5822bd..118030a6f0936e7efe9043c3792143b0b3b08405 100644
--- a/src/files.jl
+++ b/src/files.jl
@@ -85,60 +85,6 @@ getwdcontent(controller) = getwdcontent(getworkingdir(controller))
 
 # open/save
 
-function loadfile(path)
-    # TODO: refactor
-    fmt = guessfileformat(path)
-    hastags = false
-    existingtags, tagcolors = Set{Symbol}(), nothing
-    data, metadata, run = nothing, nothing, nothing
-    if fmt === :trx
-        data = read_trxmat((:spine=>Spine, :outline=>Outline, :tags=>BehaviorTags), path)
-        hastags = true
-    elseif fmt === :chore
-        data = read_chore_files((:spine=>Spine, :outline=>Outline), path)
-    elseif fmt === :json
-        data, metadata, existingtags, tagcolors = read_json_labels(path)
-        run = first(keys(data))
-        hastags = true
-    elseif fmt === :fimtrack
-        @info "Assuming 30 fps for FIMTrack v2 csv files"
-        data = read_fimtrack((:spine=>Spine, :outline=>Outline), path; fps=30)
-    else
-        return ()
-    end
-    if data isa PlanarLarvae.LarvaBase.Runs
-        @assert length(data) == 1
-        data = first(values(data))
-    end
-    # tracks
-    times = PlanarLarvae.times(data)
-    if data isa PlanarLarvae.LarvaBase.Larvae
-        tracks = [LarvaModel(id, ts, times) for (id, ts) in pairs(data)]
-    else
-        tracks = [LarvaModel(track, times) for track in data]
-    end
-    # tags
-    if hastags && isempty(existingtags)
-        for track in tracks
-            for (_, state) in track.fullstates
-                union!(existingtags, convert(Set, state.tags))
-            end
-        end
-    end
-    # dataset
-    if isnothing(metadata) || isempty(metadata)
-        metadata = extract_metadata_from_filepath(path)
-        run = get!(metadata, :date_time, "NA")
-    end
-    output = Dataset([Run(run; metadata...)])
-    #
-    if isnothing(tagcolors)
-        return (tracks=tracks, timestamps=times, tags=existingtags, output=output)
-    else
-        return (tracks=tracks, timestamps=times, tags=existingtags, tagcolors=tagcolors, output=output)
-    end
-end
-
 function tryopenfile(controller, path; reload::Bool=false)
     hub = gethub(controller)
     refresh_view = reload
@@ -200,7 +146,33 @@ function tryopenfile(controller, path; reload::Bool=false)
     end
 end
 
-checksum(filename) = bytes2hex(open(sha1, filename))
+function loadfile(path)
+    file = Loaders.load(path)
+    data = isempty(file.run) ? file.timeseries : file.run
+    # tracks
+    times = PlanarLarvae.times(data)
+    if data isa PlanarLarvae.LarvaBase.Larvae
+        tracks = [LarvaModel(id, ts, times) for (id, ts) in pairs(data)]
+    else
+        tracks = [LarvaModel(track, times) for track in values(data)]
+    end
+    # dataset
+    metadata = getmetadata(file)
+    if isempty(metadata)
+        metadata = extract_metadata_from_filepath(path)
+        run = get!(metadata, :date_time, "NA")
+    end
+    output = Dataset([Run(run; metadata...)])
+    #
+    labels = getlabels(file)
+    existingtags = labels[:names]
+    if haskey(labels, :colors)
+        tagcolors = labels[:colors]
+        return (tracks=tracks, timestamps=times, tags=existingtags, tagcolors=tagcolors, output=output)
+    else
+        return (tracks=tracks, timestamps=times, tags=existingtags, output=output)
+    end
+end
 
 getoutput(controller) = gethub(controller)[:output]
 
@@ -314,75 +286,3 @@ function getoutputfile(controller)
     end
     return outputfile
 end
-
-function read_json_labels(path)
-    run = decodelabels!(PlanarLarvae.Datasets.from_json_file(Run, path))
-    labelspec = run.attributes[:labels]
-    labelset = Symbol.(labelspec[:names])
-    tagcolors = labelspec[:colors]
-    #
-    existingtags = labelset
-    metadata = sort_metadata(run.attributes[:metadata])
-    if isempty(metadata)
-        @warn "No metadata found"
-    end
-    #
-    datadir = dirname(path)
-    datafile = get_dependencies(run, path)[1]
-    datafmt = guessfileformat(datafile)
-    data′= nothing
-    if datafmt === :trx
-        data′= read_trxmat((:spine=>Spine, :outline=>Outline), datafile)
-    elseif datafmt === :chore
-        data′= read_chore_files((:spine=>Spine, :outline=>Outline), datafile)
-    else datafmt === :fimtrack
-        @info "Assuming 30 fps for FIMTrack v2 csv files"
-        data′= read_fimtrack((:spine=>Spine, :outline=>Outline), datafile; fps=30)
-        data′= Dataset([Run(run.id, data′)])
-    end
-    @assert length(data′) == 1
-    runid = first(keys(data′))
-    if runid != run.id
-        @error "Run IDs do not match: \"$(run.id)\" vs \"$runid\""
-    end
-    recordtype = PlanarLarvae.derivedtype((:spine=>Spine, :outline=>Outline, :tags=>BehaviorTags))
-    data = PlanarLarvae.Runs{recordtype}()
-    data[runid] = PlanarLarvae.Larvae{recordtype}()
-    for (id, track′) in pairs(data′[runid])
-        newtimeseries = PlanarLarvae.TimeSeries{recordtype}()
-        if id in keys(run)
-            track = run[id]
-            tags = track[:labels]
-            for i in 1:length(track.timestamps)
-                t = track.timestamps[i]
-                tags′= BehaviorTags(labelset, begin
-                    l = tags[i]
-                    (l isa Vector) ? Symbol.(l) : [Symbol(l)]
-                end)
-                t′, state = if track′ isa Track
-                    t′= track′.timestamps[i]
-                    state = NamedTuple(track′[t′])
-                    t′, state
-                else
-                    track′[i]
-                end
-                @assert abs(t - t′) < .01
-                push!(newtimeseries, (t′, (spine=state.spine, outline=state.outline, tags=tags′)))
-            end
-        elseif track′ isa Track
-            for t′ in track′.timestamps
-                push!(newtimeseries, (t′, (spine=track′[:spine, t′], outline=track′[:outline, t′], tags=BehaviorTags(labelset, Symbol[]))))
-            end
-        else
-            for (t′, state) in track′
-                push!(newtimeseries, (t′, (spine=state.spine, outline=state.outline, tags=BehaviorTags(labelset, Symbol[]))))
-            end
-        end
-        if isempty(newtimeseries)
-            @warn "Empty time series" track=id labelled=(id in keys(run))
-        else
-            data[runid][id] = newtimeseries
-        end
-    end
-    return data, metadata, existingtags, tagcolors
-end
diff --git a/src/models.jl b/src/models.jl
index 695f7d446254ecff82aac88972aa9977781f7c0a..f8578e92e04c8f9777e2379be4fac0fd497d3e40 100644
--- a/src/models.jl
+++ b/src/models.jl
@@ -229,7 +229,7 @@ function LarvaModel(track::Track, times::Vector{PlanarLarvae.Time})
     LarvaModel(track.id,
                alignedsteps,
                path,
-               PlanarLarvae.Datasets.astimeseries(track),
+               astimeseries(track),
                usertags)
 end