Introduction
In this article, I present an exciting project that I recently realized: a media server interface built with React, using the Emby API to retrieve and display data. This project also implements a hexagonal architecture to ensure a modular organization and a clear separation of responsibilities.
During this article, we will see all the main steps of creating this project, how to work with the Emby API and how the hexagonal architecture was applied to structure the project. So let’s get started!
Presentation of the project
My goal was to develop an interface that would allow users to easily use their multimedia hosted on their Emby server. Although Emby already offers an integrated user interface, I wanted to create my own version using React and to explore new architectural approaches.
I’m sharing the project repository just below:
Github repository
git clone https://github.com/chumarie/my-media-server |
EMBY API
What is EMBY
Emby is a multimedia server that allows you to organize, manage and access your digital content such as movies, TV series and music through a user-friendly interface.
The Emby API allows developers to interact with the Emby server and access its features. It is based on HTTP requests and JSON responses, making it easy to use and integrate into various applications, including React projects. An API key is required to access data from the Emby API
How to authenticate with Emby API
Go to Settings, Advanced/Api Keys:
Create a new Api key and update the value of VITE_EMBY_SERVER_API_KEY on your .env
Go to Dashboard:
Get the Remote (WAN) access value and update VITE_EMBY_SERVER_API_URL on your .env
Once the .env file is completed, you are now ready to make API calls. Here’s how the values are used with Axios.
import axios from 'axios';
const apiKey = import.meta.env.VITE_EMBY_SERVER_API_KEY;
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_EMBY_SERVER_API_URL,
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
axiosInstance.interceptors.request.use(
config => {
config.url = `${config.url}?api_key=${apiKey}`;
return config;
},
error => {
return Promise.reject(error);
}
);
HEXAGONAL ARCHITECTURE
What is Hexagonal architecture
Hexagonal architecture, also known as port and adapter architecture, is an architectural model that aims to separate the concerns of a system into different layers to ensure modular organization and clear separation of responsibilities. This approach facilitates code maintenance, dependency management and project scalability.
Implementing hexagonal architecture in the project.
Here’s how I’ve structured my project and we’ll take a closer look at how the Category entity has been managed.
Directories structure
Application
This layer contains the business logic of the application and defines the ports, which are the interfaces between the different layers.
Create the Category Service,
This service is responsible for interacting with our models and performing actions on them
//application/services/CategoryService.ts
import { CategoryRepository } from '@domain/repositories/CategoryRepository';
/**
* A service for interacting with categories.
* @param repository - The repository to use for data access.
* @returns A CategoryRepository object with methods for retrieving categories and resumes.
*/
export const categoryService = (repository: CategoryRepository): CategoryRepository => ({
/**
* Retrieves all categories.
* @returns An array of Category objects.
*/
getCategories: () => {
return repository.getCategories();
},
});
Domain
This layer is responsible for communicating with external data sources, such as the Emby API in this case.
Create the Category Model
//domain/models/Category.ts
export type Category = {
id: string;
name: string;
image: string;
type: string;
};
Create the Category Repository
// domain/repositories/CategoryRepository.ts
import { Category } from '@domain/models/Category';
/**
* Represents a repository for categories.
*/
export interface CategoryRepository {
getCategories: () => Promise<Category[]>;
getCategoryById: (id: string) => Promise<Category[]>;
}
Infrastructure
This layer provides the services and utilities needed to run the application, such as HTTP request management, state management, and application configuration.
Create the Category DTO
// domain/infrastructure/http/dto/CategoryDTO.ts
/**
* Represents a DTO for a category.
*/
export interface CategoryDTO {
Id: string;
Name: string;
CollectionType: string;
ParentBackdropItemId: String;
}
Create the category repository
import { Http } from '@domain/repositories/Http';
import { Category } from '@domain/models/Category';
import { CategoryRepository } from '@domain/repositories/CategoryRepository';
import { CategoryDTO } from '@infrastructure/http/dto/CategoryDTO';
import { EMBY_API_KEY, EMBY_API_URL, EMBY_USER_ID } from '@infrastructure/constants/global';
/**
* Defines a categoryRepository function that takes a client of type Http and returns a CategoryRepository object
* @param client - an Http client
* @returns a CategoryRepository object
*/
export const categoryRepository = (client: Http): CategoryRepository => ({
/**
* Retrieves all categories
* @returns an array of Category objects
*/
getCategories: async () => {
const categories = await client.get<CategoryDTO>('Library/MediaFolders');
return categories.Items.map((categoryDto: CategoryDTO): Category => {
return {
id: categoryDto.Id,
name: categoryDto.Name,
image: `${EMBY_API_URL}/emby/Items/${categoryDto.Id}/Images/Primary?api_key=${EMBY_API_KEY}`,
type: categoryDto.CollectionType
};
});
},
});
Presentation
This layer includes the React components responsible for display and user interaction.
Create the Header component
And now, the component can call the categoryRepository to fetch all categories
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { categoryService } from '@application/services/CategoryService';
import { Category } from '@domain/models/Category';
import { categoryRepository } from '@infrastructure/repositories/categoryRepository';
import { httpAxios } from '@infrastructure/instances/httpAxios';
const Header = () => {
const [categories, setCategories] = useState<Category[] | null>();
const navigate = useNavigate();
const getCategories = useCallback(async () => {
try {
const responseProducts = await categoryService(categoryRepository(httpAxios)).getCategories();
setCategories(responseProducts);
} catch (exception) {
console.error(exception);
}
}, []);
Conclusion
During this project, we saw how to create a media server interface using React and the Emby API, while applying the hexagonal architecture to structure the application.
However the implementation of this architecture has also presented some challenges, such as the need to understand the basic concepts and the structure of each layer. But once mastered, I think it allows one to create a well organized, modular and easier to maintain application.
I hope this article has inspired you to explore the possibilities offered by React, Emby, and hexagonal architecture, and to consider using them in your own projects.
Would you like to read more articles by Tekos’ Team? Everything’s here.
Reference
Emby:
- Official website: https://emby.media/
- Documentation: https://github.com/MediaBrowser/Emby/wiki
- Emby API: http://swagger.emby.media/?staticview=true
Tekflix: